macos: Add updater support (#4464)
This is a very basic updater but should be enough for now. --------- Co-authored-by: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
This commit is contained in:
parent
f7c2e867f4
commit
f663a5cd38
4 changed files with 191 additions and 108 deletions
|
@ -21,6 +21,7 @@ using System.Net.Http;
|
|||
using System.Net.NetworkInformation;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -57,7 +58,7 @@ namespace Ryujinx.Modules
|
|||
// Detect current platform
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
_platformExt = "osx_x64.zip";
|
||||
_platformExt = "macos_universal.app.tar.gz";
|
||||
}
|
||||
else if (OperatingSystem.IsWindows())
|
||||
{
|
||||
|
@ -286,22 +287,40 @@ namespace Ryujinx.Modules
|
|||
|
||||
if (_updateSuccessful)
|
||||
{
|
||||
var shouldRestart = await ContentDialogHelper.CreateChoiceDialog(LocaleManager.Instance[LocaleKeys.RyujinxUpdater],
|
||||
bool shouldRestart = true;
|
||||
|
||||
if (!OperatingSystem.IsMacOS())
|
||||
{
|
||||
shouldRestart = await ContentDialogHelper.CreateChoiceDialog(LocaleManager.Instance[LocaleKeys.RyujinxUpdater],
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterCompleteMessage],
|
||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterRestartMessage]);
|
||||
}
|
||||
|
||||
if (shouldRestart)
|
||||
{
|
||||
List<string> arguments = CommandLineState.Arguments.ToList();
|
||||
string ryuName = Path.GetFileName(Environment.ProcessPath);
|
||||
string ryuExe = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ryuName);
|
||||
string executableDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
||||
string executablePath = Path.Combine(executableDirectory, ryuName);
|
||||
|
||||
if (!Path.Exists(ryuExe))
|
||||
if (!Path.Exists(executablePath))
|
||||
{
|
||||
ryuExe = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, OperatingSystem.IsWindows() ? "Ryujinx.exe" : "Ryujinx");
|
||||
executablePath = Path.Combine(executableDirectory, OperatingSystem.IsWindows() ? "Ryujinx.exe" : "Ryujinx");
|
||||
}
|
||||
|
||||
Process.Start(ryuExe, CommandLineState.Arguments);
|
||||
// On macOS we perform the update at relaunch.
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
string baseBundlePath = Path.GetFullPath(Path.Combine(executableDirectory, "..", ".."));
|
||||
string newBundlePath = Path.Combine(UpdateDir, "Ryujinx.app");
|
||||
string updaterScriptPath = Path.Combine(newBundlePath, "Contents", "Resources", "updater.sh");
|
||||
string currentPid = Process.GetCurrentProcess().Id.ToString();
|
||||
|
||||
executablePath = "/bin/bash";
|
||||
arguments.InsertRange(0, new List<string> { updaterScriptPath, baseBundlePath, newBundlePath, currentPid });
|
||||
}
|
||||
|
||||
Process.Start(executablePath, arguments);
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
|
@ -381,6 +400,15 @@ namespace Ryujinx.Modules
|
|||
|
||||
File.WriteAllBytes(updateFile, mergedFileBytes);
|
||||
|
||||
// On macOS, ensure that we remove the quarantine bit to prevent Gatekeeper from blocking execution.
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
using (Process xattrProcess = Process.Start("xattr", new List<string> { "-d", "com.apple.quarantine", updateFile }))
|
||||
{
|
||||
xattrProcess.WaitForExit();
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
InstallUpdate(taskDialog, updateFile);
|
||||
|
@ -470,29 +498,24 @@ namespace Ryujinx.Modules
|
|||
worker.Start();
|
||||
}
|
||||
|
||||
private static async void InstallUpdate(TaskDialog taskDialog, string updateFile)
|
||||
[SupportedOSPlatform("linux")]
|
||||
[SupportedOSPlatform("macos")]
|
||||
private static void ExtractTarGzipFile(TaskDialog taskDialog, string archivePath, string outputDirectoryPath)
|
||||
{
|
||||
// Extract Update
|
||||
taskDialog.SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterExtracting];
|
||||
taskDialog.SetProgressBarState(0, TaskDialogProgressState.Normal);
|
||||
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
using Stream inStream = File.OpenRead(updateFile);
|
||||
using Stream inStream = File.OpenRead(archivePath);
|
||||
using GZipInputStream gzipStream = new(inStream);
|
||||
using TarInputStream tarStream = new(gzipStream, Encoding.ASCII);
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
TarEntry tarEntry;
|
||||
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
while ((tarEntry = tarStream.GetNextEntry()) is not null)
|
||||
{
|
||||
if (tarEntry.IsDirectory) continue;
|
||||
if (tarEntry.IsDirectory)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string outPath = Path.Combine(UpdateDir, tarEntry.Name);
|
||||
string outPath = Path.Combine(outputDirectoryPath, tarEntry.Name);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outPath));
|
||||
|
||||
|
@ -515,24 +538,19 @@ namespace Ryujinx.Modules
|
|||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
taskDialog.SetProgressBarState(100, TaskDialogProgressState.Normal);
|
||||
}
|
||||
else
|
||||
private static void ExtractZipFile(TaskDialog taskDialog, string archivePath, string outputDirectoryPath)
|
||||
{
|
||||
using Stream inStream = File.OpenRead(updateFile);
|
||||
using Stream inStream = File.OpenRead(archivePath);
|
||||
using ZipFile zipFile = new(inStream);
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
double count = 0;
|
||||
foreach (ZipEntry zipEntry in zipFile)
|
||||
{
|
||||
count++;
|
||||
if (zipEntry.IsDirectory) continue;
|
||||
|
||||
string outPath = Path.Combine(UpdateDir, zipEntry.Name);
|
||||
string outPath = Path.Combine(outputDirectoryPath, zipEntry.Name);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outPath));
|
||||
|
||||
|
@ -549,9 +567,30 @@ namespace Ryujinx.Modules
|
|||
taskDialog.SetProgressBarState(GetPercentage(count, zipFile.Count), TaskDialogProgressState.Normal);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static async void InstallUpdate(TaskDialog taskDialog, string updateFile)
|
||||
{
|
||||
// Extract Update
|
||||
taskDialog.SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterExtracting];
|
||||
taskDialog.SetProgressBarState(0, TaskDialogProgressState.Normal);
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
||||
{
|
||||
ExtractTarGzipFile(taskDialog, updateFile, UpdateDir);
|
||||
}
|
||||
else if (OperatingSystem.IsWindows())
|
||||
{
|
||||
ExtractZipFile(taskDialog, updateFile, UpdateDir);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
});
|
||||
|
||||
// Delete downloaded zip
|
||||
File.Delete(updateFile);
|
||||
|
||||
|
@ -560,6 +599,9 @@ namespace Ryujinx.Modules
|
|||
taskDialog.SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterRenaming];
|
||||
taskDialog.SetProgressBarState(0, TaskDialogProgressState.Normal);
|
||||
|
||||
// NOTE: On macOS, replacement is delayed to the restart phase.
|
||||
if (!OperatingSystem.IsMacOS())
|
||||
{
|
||||
// Replace old files
|
||||
await Task.Run(() =>
|
||||
{
|
||||
|
@ -592,6 +634,7 @@ namespace Ryujinx.Modules
|
|||
});
|
||||
|
||||
Directory.Delete(UpdateDir, true);
|
||||
}
|
||||
|
||||
_updateSuccessful = true;
|
||||
|
||||
|
@ -601,7 +644,7 @@ namespace Ryujinx.Modules
|
|||
public static bool CanUpdate(bool showWarnings)
|
||||
{
|
||||
#if !DISABLE_UPDATER
|
||||
if (RuntimeInformation.OSArchitecture != Architecture.X64)
|
||||
if (RuntimeInformation.OSArchitecture != Architecture.X64 && !OperatingSystem.IsMacOS())
|
||||
{
|
||||
if (showWarnings)
|
||||
{
|
||||
|
@ -674,7 +717,7 @@ namespace Ryujinx.Modules
|
|||
#endif
|
||||
}
|
||||
|
||||
// NOTE: This method should always reflect the latest build layout.s
|
||||
// NOTE: This method should always reflect the latest build layout.
|
||||
private static IEnumerable<string> EnumerateFilesToDelete()
|
||||
{
|
||||
var files = Directory.EnumerateFiles(HomeDir); // All files directly in base dir.
|
||||
|
|
|
@ -24,6 +24,7 @@ cp $PUBLISH_DIRECTORY/*.dylib $APP_BUNDLE_DIRECTORY/Contents/Frameworks
|
|||
# Then resources
|
||||
cp Info.plist $APP_BUNDLE_DIRECTORY/Contents
|
||||
cp Ryujinx.icns $APP_BUNDLE_DIRECTORY/Contents/Resources/Ryujinx.icns
|
||||
cp updater.sh $APP_BUNDLE_DIRECTORY/Contents/Resources/updater.sh
|
||||
cp -r $PUBLISH_DIRECTORY/THIRDPARTY.md $APP_BUNDLE_DIRECTORY/Contents/Resources
|
||||
|
||||
echo -n "APPL????" > $APP_BUNDLE_DIRECTORY/Contents/PkgInfo
|
||||
|
|
|
@ -27,7 +27,7 @@ EXECUTABLE_SUB_PATH=Contents/MacOS/Ryujinx
|
|||
rm -rf $TEMP_DIRECTORY
|
||||
mkdir -p $TEMP_DIRECTORY
|
||||
|
||||
DOTNET_COMMON_ARGS="-p:DebugType=embedded -p:Version=$VERSION -p:SourceRevisionId=$SOURCE_REVISION_ID -p:ExtraDefineConstants=DISABLE_UPDATER --self-contained true"
|
||||
DOTNET_COMMON_ARGS="-p:DebugType=embedded -p:Version=$VERSION -p:SourceRevisionId=$SOURCE_REVISION_ID --self-contained true"
|
||||
|
||||
dotnet restore
|
||||
dotnet build -c Release Ryujinx.Ava
|
||||
|
|
39
distribution/macos/updater.sh
Executable file
39
distribution/macos/updater.sh
Executable file
|
@ -0,0 +1,39 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
INSTALL_DIRECTORY=$1
|
||||
NEW_APP_DIRECTORY=$2
|
||||
APP_PID=$3
|
||||
APP_ARGUMENTS="${@:4}"
|
||||
|
||||
error_handler() {
|
||||
local lineno="$1"
|
||||
|
||||
script="""
|
||||
set alertTitle to \"Ryujinx - Updater error\"
|
||||
set alertMessage to \"An error occurred during Ryujinx update (updater.sh:$lineno)\n\nPlease download the update manually from our website if the problem persists.\"
|
||||
display dialog alertMessage with icon caution with title alertTitle buttons {\"Open Download Page\", \"Exit\"}
|
||||
set the button_pressed to the button returned of the result
|
||||
|
||||
if the button_pressed is \"Open Download Page\" then
|
||||
open location \"https://ryujinx.org/download\"
|
||||
end if
|
||||
"""
|
||||
|
||||
osascript -e "$script"
|
||||
exit 1
|
||||
}
|
||||
|
||||
trap 'error_handler ${LINENO}' ERR
|
||||
|
||||
# Wait for Ryujinx to exit
|
||||
# NOTE: in case no fds are open, lsof could be returning with a process still living.
|
||||
# We wait 1s and assume the process stopped after that
|
||||
lsof -p $APP_PID +r 1 &>/dev/null
|
||||
sleep 1
|
||||
|
||||
# Now replace and reopen.
|
||||
rm -rf "$INSTALL_DIRECTORY"
|
||||
mv "$NEW_APP_DIRECTORY" "$INSTALL_DIRECTORY"
|
||||
open -a "$INSTALL_DIRECTORY" --args "$APP_ARGUMENTS"
|
Reference in a new issue