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.Net.NetworkInformation;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Runtime.Versioning;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
@ -57,7 +58,7 @@ namespace Ryujinx.Modules
|
||||||
// Detect current platform
|
// Detect current platform
|
||||||
if (OperatingSystem.IsMacOS())
|
if (OperatingSystem.IsMacOS())
|
||||||
{
|
{
|
||||||
_platformExt = "osx_x64.zip";
|
_platformExt = "macos_universal.app.tar.gz";
|
||||||
}
|
}
|
||||||
else if (OperatingSystem.IsWindows())
|
else if (OperatingSystem.IsWindows())
|
||||||
{
|
{
|
||||||
|
@ -286,22 +287,40 @@ namespace Ryujinx.Modules
|
||||||
|
|
||||||
if (_updateSuccessful)
|
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.DialogUpdaterCompleteMessage],
|
||||||
LocaleManager.Instance[LocaleKeys.DialogUpdaterRestartMessage]);
|
LocaleManager.Instance[LocaleKeys.DialogUpdaterRestartMessage]);
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldRestart)
|
if (shouldRestart)
|
||||||
{
|
{
|
||||||
|
List<string> arguments = CommandLineState.Arguments.ToList();
|
||||||
string ryuName = Path.GetFileName(Environment.ProcessPath);
|
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);
|
Environment.Exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -381,6 +400,15 @@ namespace Ryujinx.Modules
|
||||||
|
|
||||||
File.WriteAllBytes(updateFile, mergedFileBytes);
|
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
|
try
|
||||||
{
|
{
|
||||||
InstallUpdate(taskDialog, updateFile);
|
InstallUpdate(taskDialog, updateFile);
|
||||||
|
@ -470,29 +498,24 @@ namespace Ryujinx.Modules
|
||||||
worker.Start();
|
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
|
using Stream inStream = File.OpenRead(archivePath);
|
||||||
taskDialog.SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterExtracting];
|
|
||||||
taskDialog.SetProgressBarState(0, TaskDialogProgressState.Normal);
|
|
||||||
|
|
||||||
if (OperatingSystem.IsLinux())
|
|
||||||
{
|
|
||||||
using Stream inStream = File.OpenRead(updateFile);
|
|
||||||
using GZipInputStream gzipStream = new(inStream);
|
using GZipInputStream gzipStream = new(inStream);
|
||||||
using TarInputStream tarStream = new(gzipStream, Encoding.ASCII);
|
using TarInputStream tarStream = new(gzipStream, Encoding.ASCII);
|
||||||
|
|
||||||
await Task.Run(() =>
|
|
||||||
{
|
|
||||||
TarEntry tarEntry;
|
TarEntry tarEntry;
|
||||||
|
|
||||||
if (!OperatingSystem.IsWindows())
|
|
||||||
{
|
|
||||||
while ((tarEntry = tarStream.GetNextEntry()) is not null)
|
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));
|
Directory.CreateDirectory(Path.GetDirectoryName(outPath));
|
||||||
|
|
||||||
|
@ -515,24 +538,19 @@ namespace Ryujinx.Modules
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
taskDialog.SetProgressBarState(100, TaskDialogProgressState.Normal);
|
private static void ExtractZipFile(TaskDialog taskDialog, string archivePath, string outputDirectoryPath)
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
using Stream inStream = File.OpenRead(updateFile);
|
using Stream inStream = File.OpenRead(archivePath);
|
||||||
using ZipFile zipFile = new(inStream);
|
using ZipFile zipFile = new(inStream);
|
||||||
|
|
||||||
await Task.Run(() =>
|
|
||||||
{
|
|
||||||
double count = 0;
|
double count = 0;
|
||||||
foreach (ZipEntry zipEntry in zipFile)
|
foreach (ZipEntry zipEntry in zipFile)
|
||||||
{
|
{
|
||||||
count++;
|
count++;
|
||||||
if (zipEntry.IsDirectory) continue;
|
if (zipEntry.IsDirectory) continue;
|
||||||
|
|
||||||
string outPath = Path.Combine(UpdateDir, zipEntry.Name);
|
string outPath = Path.Combine(outputDirectoryPath, zipEntry.Name);
|
||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(outPath));
|
Directory.CreateDirectory(Path.GetDirectoryName(outPath));
|
||||||
|
|
||||||
|
@ -549,9 +567,30 @@ namespace Ryujinx.Modules
|
||||||
taskDialog.SetProgressBarState(GetPercentage(count, zipFile.Count), TaskDialogProgressState.Normal);
|
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
|
// Delete downloaded zip
|
||||||
File.Delete(updateFile);
|
File.Delete(updateFile);
|
||||||
|
|
||||||
|
@ -560,6 +599,9 @@ namespace Ryujinx.Modules
|
||||||
taskDialog.SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterRenaming];
|
taskDialog.SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterRenaming];
|
||||||
taskDialog.SetProgressBarState(0, TaskDialogProgressState.Normal);
|
taskDialog.SetProgressBarState(0, TaskDialogProgressState.Normal);
|
||||||
|
|
||||||
|
// NOTE: On macOS, replacement is delayed to the restart phase.
|
||||||
|
if (!OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
// Replace old files
|
// Replace old files
|
||||||
await Task.Run(() =>
|
await Task.Run(() =>
|
||||||
{
|
{
|
||||||
|
@ -592,6 +634,7 @@ namespace Ryujinx.Modules
|
||||||
});
|
});
|
||||||
|
|
||||||
Directory.Delete(UpdateDir, true);
|
Directory.Delete(UpdateDir, true);
|
||||||
|
}
|
||||||
|
|
||||||
_updateSuccessful = true;
|
_updateSuccessful = true;
|
||||||
|
|
||||||
|
@ -601,7 +644,7 @@ namespace Ryujinx.Modules
|
||||||
public static bool CanUpdate(bool showWarnings)
|
public static bool CanUpdate(bool showWarnings)
|
||||||
{
|
{
|
||||||
#if !DISABLE_UPDATER
|
#if !DISABLE_UPDATER
|
||||||
if (RuntimeInformation.OSArchitecture != Architecture.X64)
|
if (RuntimeInformation.OSArchitecture != Architecture.X64 && !OperatingSystem.IsMacOS())
|
||||||
{
|
{
|
||||||
if (showWarnings)
|
if (showWarnings)
|
||||||
{
|
{
|
||||||
|
@ -674,7 +717,7 @@ namespace Ryujinx.Modules
|
||||||
#endif
|
#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()
|
private static IEnumerable<string> EnumerateFilesToDelete()
|
||||||
{
|
{
|
||||||
var files = Directory.EnumerateFiles(HomeDir); // All files directly in base dir.
|
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
|
# Then resources
|
||||||
cp Info.plist $APP_BUNDLE_DIRECTORY/Contents
|
cp Info.plist $APP_BUNDLE_DIRECTORY/Contents
|
||||||
cp Ryujinx.icns $APP_BUNDLE_DIRECTORY/Contents/Resources/Ryujinx.icns
|
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
|
cp -r $PUBLISH_DIRECTORY/THIRDPARTY.md $APP_BUNDLE_DIRECTORY/Contents/Resources
|
||||||
|
|
||||||
echo -n "APPL????" > $APP_BUNDLE_DIRECTORY/Contents/PkgInfo
|
echo -n "APPL????" > $APP_BUNDLE_DIRECTORY/Contents/PkgInfo
|
||||||
|
|
|
@ -27,7 +27,7 @@ EXECUTABLE_SUB_PATH=Contents/MacOS/Ryujinx
|
||||||
rm -rf $TEMP_DIRECTORY
|
rm -rf $TEMP_DIRECTORY
|
||||||
mkdir -p $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 restore
|
||||||
dotnet build -c Release Ryujinx.Ava
|
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