Skip to content

Commit 89ad8e2

Browse files
authored
Merge pull request #13226 from gitbutlerapp/GB-1239
fix(but): make `but gui` work on Linux
2 parents e949c4f + 4f13171 commit 89ad8e2

3 files changed

Lines changed: 106 additions & 36 deletions

File tree

crates/but-path/src/lib.rs

Lines changed: 85 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
//! A version of `tauri::AppHandle::path()` for use outside `tauri`.
2+
#[cfg(target_os = "linux")]
3+
use std::process::Command;
24
use std::{env, path::PathBuf};
35

4-
use anyhow::{Context, bail};
6+
use anyhow::Context;
57

68
/// The directory to store application-wide data in, like logs, **one per channel**.
79
///
@@ -134,6 +136,9 @@ impl AppChannel {
134136
/// Open the GitButler GUI application for `possibly_project_dir`.
135137
///
136138
/// This uses the deeplink URL scheme registered for the specific channel.
139+
///
140+
/// Note: On Linux, we don't have a good way of distinguishing between channels in the installed
141+
/// binaries, so we always just resolve `gitbutler-tauri`.
137142
pub fn open(&self, possibly_project_dir: &std::path::Path) -> anyhow::Result<()> {
138143
let scheme = match self {
139144
AppChannel::Nightly => "but-nightly",
@@ -155,40 +160,87 @@ impl AppChannel {
155160
timestamp
156161
);
157162

158-
let mut cmd_errors = Vec::new();
159-
160-
for mut cmd in open::commands(url) {
161-
let cleaned_vars = clean_env_vars(&[
162-
"APPDIR",
163-
"GDK_PIXBUF_MODULE_FILE",
164-
"GIO_EXTRA_MODULES",
165-
"GIO_EXTRA_MODULES",
166-
"GSETTINGS_SCHEMA_DIR",
167-
"GST_PLUGIN_SYSTEM_PATH",
168-
"GST_PLUGIN_SYSTEM_PATH_1_0",
169-
"GTK_DATA_PREFIX",
170-
"GTK_EXE_PREFIX",
171-
"GTK_IM_MODULE_FILE",
172-
"GTK_PATH",
173-
"LD_LIBRARY_PATH",
174-
"PATH",
175-
"PERLLIB",
176-
"PYTHONHOME",
177-
"PYTHONPATH",
178-
"QT_PLUGIN_PATH",
179-
"XDG_DATA_DIRS",
180-
]);
181-
182-
cmd.envs(cleaned_vars);
163+
let cleaned_vars: Vec<(&str, String)> = clean_env_vars(&[
164+
"APPDIR",
165+
"GDK_PIXBUF_MODULE_FILE",
166+
"GIO_EXTRA_MODULES",
167+
"GSETTINGS_SCHEMA_DIR",
168+
"GST_PLUGIN_SYSTEM_PATH",
169+
"GST_PLUGIN_SYSTEM_PATH_1_0",
170+
"GTK_DATA_PREFIX",
171+
"GTK_EXE_PREFIX",
172+
"GTK_IM_MODULE_FILE",
173+
"GTK_PATH",
174+
"LD_LIBRARY_PATH",
175+
"PATH",
176+
"PERLLIB",
177+
"PYTHONHOME",
178+
"PYTHONPATH",
179+
"QT_PLUGIN_PATH",
180+
"XDG_DATA_DIRS",
181+
])
182+
.collect();
183+
184+
#[cfg(target_os = "linux")]
185+
{
186+
// On Linux, we don't currently want to rely on the scheme being properly registered
187+
// with the Tauri app. The mechanism by which the scheme is registered relies on the
188+
// bundled Desktop entry, and different desktop environments have pretty wildly
189+
// different handling of such entries.
190+
//
191+
// Adding insult to injury, even if that desktop entry is resolved, it in turn just has
192+
// an exec line with the name `gitbutler-tauri` and the URL is provided as a command
193+
// line argument. Therefore, after the roundabout trip to the desktop entry via the
194+
// custom scheme, we _still_ just resolve `gitbutler-tauri` from PATH.
195+
//
196+
// Even more annoying is that the desktop entry does not have a placeholder for the
197+
// command line argument, causing some stricter environments such as KDE to just error
198+
// out, while some more lenient environments simply append the URL to the exec line.
199+
//
200+
// For these reasons, it's way more reliable and simpler to just try to call
201+
// `gitbutler-tauri` directly, completely circumventing any issues with scheme
202+
// registration.
203+
//
204+
// As the binary is always called `gitbutler-tauri`, there's currently no way to
205+
// distinguish between release, nightly and dev. We'll just have to try to launch
206+
// whatever we find. This can be fixed by giving the binaries different names, but as
207+
// so few users use nightly builds, it's just not worth the effort.
208+
let mut cmd = Command::new("gitbutler-tauri");
209+
cmd.arg(&url);
183210
cmd.current_dir(env::temp_dir());
184-
if cmd.status().is_ok() {
185-
return Ok(());
186-
} else {
187-
cmd_errors.push(anyhow::anyhow!("Failed to execute command {cmd:?}"));
211+
cmd.envs(cleaned_vars.clone());
212+
213+
// Unset all io to not pollute the terminal with output.
214+
cmd.stdin(std::process::Stdio::null());
215+
cmd.stdout(std::process::Stdio::null());
216+
cmd.stderr(std::process::Stdio::null());
217+
218+
// We spawn this fire-and-forget style. The process will be re-parented to init when
219+
// the caller exits (and that caller is typically the `but` CLI). This allows you to
220+
// e.g. run `but gui` in a terminal, and then keep using that terminal.
221+
//
222+
// This is only necessary on cold start, i.e. when the GUI isn't already running, as
223+
// then this process becomes the GUI process. If the GUI is already running, this
224+
// process effectively just sends the deep link to the already running GUI and then
225+
// exits.
226+
cmd.spawn()?;
227+
};
228+
229+
#[cfg(not(target_os = "linux"))]
230+
{
231+
let mut cmd_errors = Vec::new();
232+
for mut cmd in open::commands(&url) {
233+
cmd.envs(cleaned_vars.clone());
234+
cmd.current_dir(env::temp_dir());
235+
if cmd.status().is_ok() {
236+
return Ok(());
237+
} else {
238+
cmd_errors.push(anyhow::anyhow!("Failed to execute command {cmd:?}"));
239+
}
240+
}
241+
if !cmd_errors.is_empty() {
242+
anyhow::bail!("Errors occurred: {cmd_errors:?}");
188243
}
189-
}
190-
if !cmd_errors.is_empty() {
191-
bail!("Errors occurred: {cmd_errors:?}");
192244
}
193245
Ok(())
194246
}

crates/but/src/setup.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,19 @@ fn spawn_background_sync(
324324
operations: SyncOperations,
325325
silent: bool,
326326
) {
327-
let binary_path = std::env::current_exe().unwrap_or_default();
327+
#[cfg(windows)]
328+
let binary_path = std::env::current_exe().unwrap();
329+
#[cfg(unix)]
330+
// std::env::current_exe() resolves symlinks on many UNIX implementations, which breaks the
331+
// builtin-but behavior that relies on being able to tell that a `but` symlink was used to start
332+
// `gitbutler-tauri`. Getting the actual OS argument used to invoke the program circumvents this
333+
// issue.
334+
//
335+
// It would in theory be possible to just fork the current process, instead of fork-exec like we
336+
// currently do here. But that would split the implementation across UNIX and Windows (which
337+
// does not have fork), and I'm also uncertain how the Tokio runtime would behave with a fork.
338+
let binary_path = std::env::args_os().next().unwrap();
339+
328340
let mut cmd = tokio::process::Command::new(binary_path);
329341
cmd.arg("-C")
330342
.arg(&args.current_dir)

crates/gitbutler-tauri/src/main.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,14 @@ fn main() -> anyhow::Result<()> {
5454
.build()?;
5555
#[cfg(feature = "builtin-but")]
5656
{
57-
let exe = std::env::current_exe()?;
58-
if exe.file_stem().is_some_and(|stem| stem == "but") || std::env::args_os().count() > 1 {
57+
// Note: We use std::env::args_os() instead of std::env::current_exe() as the latter
58+
// resolves symlinks on many UNIX implementations, which makes it unsuitable for the `but`
59+
// symlink trick.
60+
if std::env::args_os().next().is_some_and(|exec_path| {
61+
std::path::Path::new(&exec_path)
62+
.file_stem()
63+
.is_some_and(|stem| stem == "but")
64+
}) {
5965
gitbutler_repo_actions::askpass::disable();
6066
return runtime.block_on(but::handle_args(std::env::args_os()));
6167
}

0 commit comments

Comments
 (0)