diff --git a/scripts/util/Platform.cs b/scripts/util/Platform.cs
new file mode 100644
index 00000000..593d4669
--- /dev/null
+++ b/scripts/util/Platform.cs
@@ -0,0 +1,22 @@
+using System.IO;
+using Godot;
+
+namespace util;
+
+public static class Platform
+{
+ ///
+ /// Path to the running .app bundle on macOS, the executable lives at Rhythia.app/Contents/MacOS/Rhythia
+ ///
+ public static string MacAppBundlePath => Path.GetFullPath(Path.Combine(OS.GetExecutablePath(), "../../.."));
+
+ ///
+ /// Wraps a value in single quotes so it is passed to /bin/sh as one literal argument
+ ///
+ public static string ShellQuote(string value) => $"'{value.Replace("'", "'\\''")}'";
+
+ ///
+ /// Shell snippet that blocks until the current process has exited
+ ///
+ public static string WaitForExitScript => $"while kill -0 {OS.GetProcessId()} 2>/dev/null; do sleep 0.1; done";
+}
diff --git a/scripts/util/Platform.cs.uid b/scripts/util/Platform.cs.uid
new file mode 100644
index 00000000..8124b9d7
--- /dev/null
+++ b/scripts/util/Platform.cs.uid
@@ -0,0 +1 @@
+uid://d0b88m1ycctvg
diff --git a/scripts/util/Releases.cs b/scripts/util/Releases.cs
index aa0bfce0..a6089748 100644
--- a/scripts/util/Releases.cs
+++ b/scripts/util/Releases.cs
@@ -4,6 +4,7 @@
using Godot;
using Octokit;
using Updatum;
+using util;
public class Releases
{
@@ -50,6 +51,36 @@ public static async Task InstallUpdateAsync(UpdatumDownloadedAsset asset)
return false;
}
+ if (OS.GetName() == "macOS")
+ {
+ return installMacUpdate(asset);
+ }
+
return await MANAGER.InstallUpdateAsync(asset);
}
+
+ // Updatum finds the .app through AppContext.BaseDirectory, which Godot points at Contents/Resources,
+ // so it would sync the update inside the running bundle instead of replacing it
+ private static bool installMacUpdate(UpdatumDownloadedAsset asset)
+ {
+ if (asset == null || !asset.FileExists)
+ {
+ return false;
+ }
+
+ string app = Platform.ShellQuote(Platform.MacAppBundlePath);
+ string zip = Platform.ShellQuote(asset.FilePath);
+
+ string script =
+ $"{Platform.WaitForExitScript}; "
+ + $"tmp=$(mktemp -d) && ditto -x -k {zip} \"$tmp\" "
+ + "&& new=$(find \"$tmp\" -maxdepth 1 -name '*.app' | head -n 1) && [ -n \"$new\" ] "
+ + $"&& mv {app} \"$tmp/old.app\" && {{ ditto \"$new\" {app} || mv \"$tmp/old.app\" {app}; }}; "
+ + $"xattr -dr com.apple.quarantine {app}; rm -rf \"$tmp\"; open {app}";
+
+ OS.CreateProcess("/bin/sh", ["-c", script]);
+ ((SceneTree)Engine.GetMainLoop()).Quit();
+
+ return true;
+ }
}