Fix Ghostty SSH wrapper path in embedded app bundles (#8109)

* fix: resolve Ghostty SSH helper in embedded bundles

* test: cover Ghostty fish SSH feature variants
This commit is contained in:
Austin Wang
2026-07-14 20:38:53 -07:00
committed by GitHub
parent cf18c5918f
commit be6ea18ccf
5 changed files with 94 additions and 15 deletions
@@ -154,7 +154,12 @@ extension TerminalSurface {
setManagedEnvironmentValue("CMUX_AMP_HOOKS_DISABLED", "1")
}
if let cliBinPath = Bundle.main.resourceURL?.appendingPathComponent("bin").path {
if let cliBinURL = Bundle.main.resourceURL?.appendingPathComponent("bin") {
let cliBinPath = cliBinURL.path
let ghosttyCLIPath = cliBinURL.appendingPathComponent("ghostty").path
if FileManager.default.isExecutableFile(atPath: ghosttyCLIPath) {
setManagedEnvironmentValue("GHOSTTY_BIN", ghosttyCLIPath)
}
let currentPath = env["PATH"]
?? getenv("PATH").map { String(cString: $0) }
?? ProcessInfo.processInfo.environment["PATH"]
+26 -6
View File
@@ -12,12 +12,32 @@ When we change the fork, update this document and the parent submodule SHA.
## Current fork changes
Current cmux pinned fork head: `eb500e9f4`. It advances the previous cmux pin
`5ae712a89` through the bounded-scrollback merge `81a6daa8e`, then adds
terminal-owned scrollbar snapshots, absolute row-space identity, OSC-boundary
geometry, and compare-and-set absolute-row restoration for notification
scrollback replay. The commit is reachable from fork `main` through
`cbbddb292`.
Current cmux pinned fork head: `b4b6d69c8`. It advances the previous cmux pin
`a6305908a` with an exact Ghostty CLI executable-path contract for embedded
hosts. The commit is reachable from fork `main` through `67b388b73` and was
published via https://github.com/manaflow-ai/ghostty/pull/115.
The corresponding universal ReleaseFast GhosttyKit archive is published at
https://github.com/manaflow-ai/ghostty/releases/tag/xcframework-b4b6d69c82033e16137266a04b364dc53d16c350-crashsubdir-cmux-crash-v1
and pinned in `scripts/ghosttykit-checksums.txt`.
### Embedded Ghostty CLI path ownership
- `src/termio/Exec.zig` exports `GHOSTTY_BIN` as the exact CLI executable.
Native Ghostty resolves to its running binary; an embedded host can supply a
separate helper without assuming the host GUI executable is named `ghostty`.
- The zsh, bash, fish, nushell, and elvish SSH integrations invoke
`GHOSTTY_BIN` directly. They install no SSH wrapper when an embedded host has
not supplied a helper, so missing optional CLI support cannot break ordinary
`ssh`.
- `GHOSTTY_BIN_DIR` remains the directory contract for the independent `path`
shell-integration feature; it is no longer used to reconstruct a CLI filename.
- Conflict note: future upstream merges must preserve the distinction between
the exact CLI path (`GHOSTTY_BIN`) and its PATH directory
(`GHOSTTY_BIN_DIR`) across `src/termio/Exec.zig` and every shell integration.
The earlier fork history below includes terminal-owned scrollbar snapshots,
absolute row-space identity, OSC-boundary geometry, and compare-and-set
absolute-row restoration for notification scrollback replay.
The underlying compression, selection, and full-scrollback changes were
published via
+1 -1
Submodule ghostty updated: a6305908a3...b4b6d69c82
+1
View File
@@ -63,3 +63,4 @@ e215e78bf04df3f7cecbef665eec051a203baf6a 308161b30d9111e84d039326fbf314d52340c75
5ae712a89479f16d47d9a75e1a802a22415a3033 4cd7b251534890b63a83ac49feef2b99a835af88d8a1c7523df5aa1a6caa1d02
eb500e9f45c8b6ffa6043350ec1488a42d195406 47810027ffd9bc19873752d3698da2d60899f2140dacf3271f826de0c958178e
a6305908a39646ed87764aadd4ec8364c1d939a0 93f0b2b8dd992dad80b8def0d4b7e2b8f1ade7aa6df600987d9c4cb1e893b394
b4b6d69c82033e16137266a04b364dc53d16c350 89a2d738fc566c9c91e2c0ed7c2c9fa9f6ac18d9f0541fd9ff58e41bb8b8caa9
@@ -9,6 +9,7 @@ from __future__ import annotations
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
@@ -16,10 +17,20 @@ import tempfile
ROOT = Path(__file__).resolve().parents[1]
ZSH_INTEGRATION = ROOT / "ghostty/src/shell-integration/zsh/ghostty-integration"
BASH_INTEGRATION = ROOT / "ghostty/src/shell-integration/bash/ghostty.bash"
EXPECTED_ARGUMENTS = ["+ssh", "--", "[email protected]"]
FISH_INTEGRATION = (
ROOT
/ "ghostty/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish"
)
def _run_wrapper(shell: str, integration: Path, helper: Path, log: Path) -> None:
def _run_wrapper(
shell: str,
integration: Path,
helper: Path,
log: Path,
features: str,
expected_arguments: list[str],
) -> None:
env = os.environ.copy()
env.update(
{
@@ -27,7 +38,7 @@ def _run_wrapper(shell: str, integration: Path, helper: Path, log: Path) -> None
# Reproduce cmux's GUI executable directory. No `ghostty` binary
# exists here because cmux embeds GhosttyKit in its own executable.
"GHOSTTY_BIN_DIR": str(helper.parents[2] / "MacOS"),
"GHOSTTY_SHELL_FEATURES": "ssh-env,ssh-terminfo",
"GHOSTTY_SHELL_FEATURES": features,
"GHOSTTY_TEST_LOG": str(log),
}
)
@@ -45,7 +56,7 @@ def _run_wrapper(shell: str, integration: Path, helper: Path, log: Path) -> None
"zsh",
str(integration),
]
else:
elif shell == "bash":
command = [
"bash",
"--noprofile",
@@ -55,6 +66,23 @@ def _run_wrapper(shell: str, integration: Path, helper: Path, log: Path) -> None
"bash",
str(integration),
]
else:
command = [
"fish",
"--no-config",
"--interactive",
"--command",
(
'source "$argv[1]"; '
"emit fish_prompt >/dev/null; "
"functions -q ssh; or begin; "
'echo "fish SSH wrapper was not installed" >&2; '
"exit 97; "
"end; "
"ssh [email protected]"
),
str(integration),
]
result = subprocess.run(command, env=env, text=True, capture_output=True)
if result.returncode != 0:
@@ -64,10 +92,10 @@ def _run_wrapper(shell: str, integration: Path, helper: Path, log: Path) -> None
)
actual = log.read_text().splitlines() if log.exists() else []
if actual != EXPECTED_ARGUMENTS:
if actual != expected_arguments:
raise AssertionError(
f"{shell} SSH wrapper invoked the wrong executable or arguments: "
f"expected {EXPECTED_ARGUMENTS!r}, got {actual!r}"
f"expected {expected_arguments!r}, got {actual!r}"
)
@@ -86,7 +114,32 @@ def main() -> None:
("bash", BASH_INTEGRATION),
):
log = tmp / f"{shell}-argv.log"
_run_wrapper(shell, integration, helper, log)
_run_wrapper(
shell,
integration,
helper,
log,
"ssh-env,ssh-terminfo",
["+ssh", "--", "[email protected]"],
)
if shutil.which("fish") is None:
print("SKIP: fish is not installed; fish SSH wrappers were not exercised")
else:
for features, expected_flags in (
("ssh-env", ["--terminfo=false"]),
("ssh-terminfo", ["--forward-env=false"]),
("ssh-env,ssh-terminfo", []),
):
log = tmp / f"fish-{features}.log"
_run_wrapper(
"fish",
FISH_INTEGRATION,
helper,
log,
features,
["+ssh", *expected_flags, "--", "[email protected]"],
)
print("PASS: Ghostty SSH wrappers invoke the host-provided executable path")