v0.12.0:1 - hotfix: WhisperX install fails on first scp because ~ doesn't

expand inside shlex.quote()

Symptom: "Failed to ship Dockerfile — bash: line 1: ~/whisperx-build/
Dockerfile: No such file or directory"

Same bug pattern as v0.8.1:1 (disk probe). shlex.quote() wraps in single
quotes, and the remote shell doesn't do tilde expansion inside single
quotes — so it tries to write to a literal directory named "~".

Fix: use $HOME in double-quoted shell context, which the remote shell
expands correctly. The file names (Dockerfile, requirements.txt, etc.)
are hardcoded so they're safe to embed unquoted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Keysat
2026-05-18 21:16:44 -05:00
parent 5a0bfba6a3
commit ce5aee1920
2 changed files with 21 additions and 9 deletions
+19 -7
View File
@@ -163,33 +163,45 @@ class WhisperXInstaller:
s = self.settings
host = s.whisperx_host
user = s.whisperx_user
build_dir = "~/whisperx-build"
# NOTE: `~` does not expand inside shlex.quote() single-quotes (bit us
# in v0.12.0:0). Use a $HOME-relative path that the REMOTE shell
# expands; all path components are hardcoded so injection is moot.
build_dir_remote = "\"$HOME\"/whisperx-build"
build_dir_display = "~/whisperx-build"
# ── Phase 1: stage build context on Spark 2 ──
job.state = "sending"
job.phase = "Sending build context to Spark 2…"
job.append(f"$ ssh {user}@{host} 'mkdir -p {build_dir}/app'")
rc, out, err = await ssh_run(host, user, f"mkdir -p {build_dir}/app && rm -f {build_dir}/Dockerfile {build_dir}/requirements.txt {build_dir}/README.md {build_dir}/app/main.py", s, timeout=10)
job.append(f"$ ssh {user}@{host} 'mkdir -p {build_dir_display}/app'")
rc, out, err = await ssh_run(
host, user,
f"mkdir -p {build_dir_remote}/app && "
f"rm -f {build_dir_remote}/Dockerfile {build_dir_remote}/requirements.txt "
f"{build_dir_remote}/README.md {build_dir_remote}/app/main.py",
s, timeout=10,
)
if rc != 0:
job.append(f"[mkdir failed] {err.strip()}")
raise RuntimeError("failed to create build directory")
for local_name, remote_rel in BUILD_FILES.items():
local_path = BUILD_CONTEXT_DIR / local_name
body = local_path.read_bytes()
remote_path = f"{build_dir}/{remote_rel}"
cmd = f"cat > {shlex.quote(remote_path)}"
remote_path_for_shell = f"{build_dir_remote}/{remote_rel}"
# remote_rel is hardcoded ("Dockerfile" / "app/main.py" etc.) — safe
# to embed unquoted inside the double-quoted $HOME path.
cmd = f"cat > {remote_path_for_shell}"
ok, out, err = await self._ssh_pipe(host, user, cmd, body, timeout=30)
if not ok:
job.append(f"[scp {local_name} failed] {err.strip()[:200]}")
raise RuntimeError(f"failed to ship {local_name}")
job.append(f"{remote_path} ({len(body)} bytes)")
job.append(f"{build_dir_display}/{remote_rel} ({len(body)} bytes)")
# ── Phase 2: docker build ──
job.state = "building"
job.phase = "Building Docker image on Spark 2 (this is the slow part — 515 min if base layers aren't cached)…"
build_cmd = (
f"set -e; "
f"cd {build_dir}; "
f"cd {build_dir_remote}; "
f"echo '=== docker build -t {s.whisperx_container}:latest . ==='; "
f"docker build -t {s.whisperx_container}:latest ."
)