Skip to content

Commit d6e2cef

Browse files
committed
fix: bootstrap ghostty runtime for registry installs
1 parent 429f2ed commit d6e2cef

10 files changed

Lines changed: 1244 additions & 95 deletions

File tree

Cargo.lock

Lines changed: 613 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/taskers-app/src/main.rs

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ use std::{
88
collections::{HashMap, HashSet},
99
future::pending,
1010
path::PathBuf,
11+
process::{Command, Stdio},
1112
rc::Rc,
1213
thread,
13-
time::Duration,
14+
time::{Duration, Instant},
1415
};
1516

1617
use adw::prelude::*;
@@ -36,6 +37,7 @@ use taskers_domain::{
3637
};
3738
use taskers_ghostty::{
3839
BackendChoice, BackendProbe, DefaultBackend, GhosttyHost, SurfaceDescriptor, TerminalBackend,
40+
ensure_runtime_installed,
3941
};
4042
use taskers_runtime::{
4143
ShellLaunchSpec, default_shell_program, install_shell_integration, validate_shell_program,
@@ -55,6 +57,8 @@ struct Cli {
5557
clean_shell: bool,
5658
#[arg(long, default_value_t = false, conflicts_with = "clean_shell")]
5759
raw_shell: bool,
60+
#[arg(long, hide = true, default_value_t = false)]
61+
internal_ghostty_probe: bool,
5862
}
5963

6064
struct StartupContext {
@@ -1462,6 +1466,9 @@ impl UiHandle {
14621466

14631467
fn main() -> gtk::glib::ExitCode {
14641468
let cli = Cli::parse();
1469+
if cli.internal_ghostty_probe {
1470+
return run_internal_ghostty_probe();
1471+
}
14651472
let shell_mode_non_unique = cli.clean_shell || cli.raw_shell;
14661473
let run_non_unique = shell_mode_non_unique
14671474
|| cli.socket.is_some()
@@ -1472,6 +1479,14 @@ fn main() -> gtk::glib::ExitCode {
14721479
.session
14731480
.unwrap_or_else(session_store::default_session_path);
14741481
let config_path = settings_store::default_config_path();
1482+
let ghostty_runtime_toast = match ensure_runtime_installed() {
1483+
Ok(Some(runtime)) => Some(format!(
1484+
"Installed Ghostty runtime assets to {}",
1485+
runtime.runtime_dir.display()
1486+
)),
1487+
Ok(None) => None,
1488+
Err(error) => Some(format!("Ghostty runtime bootstrap unavailable: {error}")),
1489+
};
14751490
let probe = DefaultBackend::probe(BackendChoice::Auto);
14761491
if cli.clean_shell {
14771492
unsafe {
@@ -1523,7 +1538,7 @@ fn main() -> gtk::glib::ExitCode {
15231538
initialize_terminal_backend(&probe);
15241539
let startup_toast = merge_startup_toasts(
15251540
merge_startup_toasts(
1526-
shell_integration_toast,
1541+
merge_startup_toasts(ghostty_runtime_toast, shell_integration_toast),
15271542
cli.clean_shell
15281543
.then(|| "Using clean shell startup".to_string()),
15291544
),
@@ -1657,6 +1672,15 @@ fn initialize_terminal_backend(
16571672
return (BackendChoice::Mock, probe.notes.clone(), None, None);
16581673
}
16591674

1675+
if let Err(error) = probe_ghostty_backend_process() {
1676+
let note = format!(
1677+
"{} Ghostty self-probe failed; using placeholder terminal surfaces.",
1678+
probe.notes
1679+
);
1680+
let toast = format!("Ghostty backend unavailable: {error}");
1681+
return (BackendChoice::Mock, note, None, Some(toast));
1682+
}
1683+
16601684
match GhosttyHost::new() {
16611685
Ok(host) => (
16621686
BackendChoice::Ghostty,
@@ -1672,6 +1696,67 @@ fn initialize_terminal_backend(
16721696
}
16731697
}
16741698

1699+
fn run_internal_ghostty_probe() -> gtk::glib::ExitCode {
1700+
match GhosttyHost::new() {
1701+
Ok(host) => {
1702+
let _ = host.tick();
1703+
thread::sleep(Duration::from_millis(250));
1704+
gtk::glib::ExitCode::SUCCESS
1705+
}
1706+
Err(error) => {
1707+
eprintln!("ghostty self-probe failed: {error}");
1708+
gtk::glib::ExitCode::FAILURE
1709+
}
1710+
}
1711+
}
1712+
1713+
fn probe_ghostty_backend_process() -> Result<(), String> {
1714+
let current_exe = std::env::current_exe()
1715+
.map_err(|error| format!("failed to resolve current executable: {error}"))?;
1716+
let mut child = Command::new(current_exe)
1717+
.arg("--internal-ghostty-probe")
1718+
.stdin(Stdio::null())
1719+
.stdout(Stdio::null())
1720+
.stderr(Stdio::null())
1721+
.spawn()
1722+
.map_err(|error| format!("failed to launch Ghostty self-probe: {error}"))?;
1723+
1724+
let deadline = Instant::now() + Duration::from_secs(5);
1725+
loop {
1726+
match child.try_wait() {
1727+
Ok(Some(status)) => {
1728+
if status.success() {
1729+
return Ok(());
1730+
}
1731+
return Err(describe_exit_status(status));
1732+
}
1733+
Ok(None) if Instant::now() < deadline => thread::sleep(Duration::from_millis(50)),
1734+
Ok(None) => {
1735+
let _ = child.kill();
1736+
let _ = child.wait();
1737+
return Err("Ghostty self-probe timed out".into());
1738+
}
1739+
Err(error) => return Err(format!("failed to wait for Ghostty self-probe: {error}")),
1740+
}
1741+
}
1742+
}
1743+
1744+
fn describe_exit_status(status: std::process::ExitStatus) -> String {
1745+
#[cfg(unix)]
1746+
{
1747+
use std::os::unix::process::ExitStatusExt;
1748+
1749+
if let Some(signal) = status.signal() {
1750+
return format!("Ghostty self-probe crashed with signal {signal}");
1751+
}
1752+
}
1753+
1754+
match status.code() {
1755+
Some(code) => format!("Ghostty self-probe exited with status {code}"),
1756+
None => "Ghostty self-probe exited unsuccessfully".into(),
1757+
}
1758+
}
1759+
16751760
fn merge_startup_toasts(first: Option<String>, second: Option<String>) -> Option<String> {
16761761
match (first, second) {
16771762
(Some(first), Some(second)) => Some(format!("{first}\n{second}")),

crates/taskers-cli/src/main.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -965,6 +965,8 @@ fn install_ghostty_runtime(
965965
"-Dapp-runtime=gtk",
966966
"-Demit-exe=false",
967967
"-Dgtk-wayland=false",
968+
"-Dstrip=true",
969+
"-Di18n=false",
968970
"--summary",
969971
"none",
970972
"--prefix",
@@ -1008,6 +1010,16 @@ fn install_ghostty_runtime(
10081010
embedded_terminfo_dir.display()
10091011
)
10101012
})?;
1013+
std::fs::write(
1014+
resources_dir.join(".taskers-runtime-version"),
1015+
env!("CARGO_PKG_VERSION"),
1016+
)
1017+
.with_context(|| {
1018+
format!(
1019+
"failed to write Ghostty runtime version marker to {}",
1020+
resources_dir.join(".taskers-runtime-version").display()
1021+
)
1022+
})?;
10111023

10121024
Ok(())
10131025
}

crates/taskers-ghostty/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,14 @@ build = "build.rs"
1313
gtk.workspace = true
1414
libloading = "0.8"
1515
serde.workspace = true
16+
tar = "0.4"
1617
thiserror.workspace = true
18+
ureq = "2.12"
19+
xz2 = "0.1"
1720

1821
[features]
1922
default = []
2023
libghostty = []
24+
25+
[dev-dependencies]
26+
tempfile.workspace = true

crates/taskers-ghostty/build.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ fn main() {
1818
if env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("linux") {
1919
return;
2020
}
21+
println!("cargo:rustc-cfg=taskers_ghostty_bridge");
22+
if let Ok(target) = env::var("TARGET") {
23+
println!("cargo:rustc-env=TASKERS_BUILD_TARGET={target}");
24+
}
2125

2226
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("manifest dir"));
2327
let workspace_root = manifest_dir
@@ -28,7 +32,7 @@ fn main() {
2832
let vendor_dir = workspace_root.join("vendor").join("ghostty");
2933
if !vendor_dir.exists() {
3034
println!(
31-
"cargo:warning=vendored Ghostty source tree not found; building without the Ghostty bridge"
35+
"cargo:warning=vendored Ghostty source tree not found; runtime bundle bootstrap will be required"
3236
);
3337
return;
3438
}
@@ -60,6 +64,8 @@ fn build_bridge(vendor_dir: &Path, install_dir: &Path) {
6064
"-Dapp-runtime=gtk",
6165
"-Demit-exe=false",
6266
"-Dgtk-wayland=false",
67+
"-Dstrip=true",
68+
"-Di18n=false",
6369
"--summary",
6470
"none",
6571
"--prefix",

crates/taskers-ghostty/src/backend.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::bridge::{runtime_bridge_path, runtime_resources_dir};
1+
use crate::runtime::{runtime_bridge_path, runtime_resources_dir};
22
use serde::{Deserialize, Serialize};
33
use std::collections::BTreeMap;
44
use thiserror::Error;

crates/taskers-ghostty/src/bridge.rs

Lines changed: 1 addition & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use libloading::Library;
1919
use thiserror::Error;
2020

2121
use crate::backend::SurfaceDescriptor;
22+
use crate::runtime::{configure_runtime_environment, runtime_bridge_path};
2223

2324
#[derive(Debug, Error)]
2425
pub enum GhosttyError {
@@ -197,85 +198,6 @@ impl Drop for GhosttyHost {
197198
}
198199
}
199200

200-
pub fn configure_runtime_environment() {
201-
if std::env::var_os("GHOSTTY_RESOURCES_DIR").is_some() {
202-
return;
203-
}
204-
205-
if let Some(path) = installed_runtime_dir().filter(|path| path.exists()) {
206-
unsafe {
207-
std::env::set_var("GHOSTTY_RESOURCES_DIR", &path);
208-
}
209-
return;
210-
}
211-
212-
if let Some(path) = option_env!("TASKERS_GHOSTTY_BUILD_RESOURCES_DIR")
213-
.map(PathBuf::from)
214-
.filter(|path| path.exists())
215-
{
216-
unsafe {
217-
std::env::set_var("GHOSTTY_RESOURCES_DIR", &path);
218-
}
219-
}
220-
}
221-
222-
pub fn runtime_resources_dir() -> Option<PathBuf> {
223-
if let Some(path) = std::env::var_os("GHOSTTY_RESOURCES_DIR")
224-
.map(PathBuf::from)
225-
.filter(|path| path.exists())
226-
{
227-
return Some(path);
228-
}
229-
230-
if let Some(path) = installed_runtime_dir().filter(|path| path.exists()) {
231-
return Some(path);
232-
}
233-
234-
option_env!("TASKERS_GHOSTTY_BUILD_RESOURCES_DIR")
235-
.map(PathBuf::from)
236-
.filter(|path| path.exists())
237-
}
238-
239-
pub fn runtime_bridge_path() -> Option<PathBuf> {
240-
if let Some(path) = std::env::var_os("TASKERS_GHOSTTY_BRIDGE_PATH")
241-
.map(PathBuf::from)
242-
.filter(|path| path.exists())
243-
{
244-
return Some(path);
245-
}
246-
247-
if let Some(path) = installed_runtime_dir()
248-
.map(|root| root.join("lib").join("libtaskers_ghostty_bridge.so"))
249-
.filter(|path| path.exists())
250-
{
251-
return Some(path);
252-
}
253-
254-
option_env!("TASKERS_GHOSTTY_BUILD_BRIDGE_PATH")
255-
.map(PathBuf::from)
256-
.filter(|path| path.exists())
257-
}
258-
259-
fn installed_runtime_dir() -> Option<PathBuf> {
260-
if let Some(path) = std::env::var_os("TASKERS_GHOSTTY_RUNTIME_DIR").map(PathBuf::from) {
261-
return Some(path);
262-
}
263-
264-
if let Some(path) = std::env::var_os("XDG_DATA_HOME")
265-
.map(PathBuf::from)
266-
.map(|path| path.join("taskers").join("ghostty"))
267-
{
268-
return Some(path);
269-
}
270-
271-
std::env::var_os("HOME").map(PathBuf::from).map(|path| {
272-
path.join(".local")
273-
.join("share")
274-
.join("taskers")
275-
.join("ghostty")
276-
})
277-
}
278-
279201
#[cfg(taskers_ghostty_bridge)]
280202
fn load_bridge_library() -> Result<GhosttyBridgeLibrary, GhosttyError> {
281203
let path = runtime_bridge_path().ok_or(GhosttyError::LibraryPathUnavailable)?;

crates/taskers-ghostty/src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
pub mod backend;
22
pub mod bridge;
3+
pub mod runtime;
34

45
pub use backend::{
56
AdapterError, BackendAvailability, BackendChoice, BackendProbe, DefaultBackend,
67
SurfaceDescriptor, TerminalBackend,
78
};
8-
pub use bridge::{GhosttyError, GhosttyHost, configure_runtime_environment};
9+
pub use bridge::{GhosttyError, GhosttyHost};
10+
pub use runtime::{
11+
RuntimeBootstrap, RuntimeBootstrapError, configure_runtime_environment,
12+
ensure_runtime_installed,
13+
};

0 commit comments

Comments
 (0)