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:
+6
-1
@@ -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
@@ -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
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user