From 46f5d1d31aa75a4eb8245df65652a8151e211213 Mon Sep 17 00:00:00 2001 From: LotP Date: Wed, 22 Apr 2026 21:13:34 +0000 Subject: [PATCH] updated decompression support (#26) - update decompression functions to use native libs - add support for 7zip and tar.xz archives with SharpCompress - add .7z and tar.xz artifacts to releases - remove linux non-appimage builds from PRs Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/26 --- .forgejo/workflows/build.yml | 31 ++-- .forgejo/workflows/canary.yml | 2 + .forgejo/workflows/release.yml | 2 + Directory.Packages.props | 4 +- src/Ryujinx/Ryujinx.csproj | 2 +- src/Ryujinx/Systems/Updater/Updater.GitLab.cs | 4 +- src/Ryujinx/Systems/Updater/Updater.cs | 174 ++++++++---------- 7 files changed, 102 insertions(+), 117 deletions(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 3a23f2ae8..a7db99713 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -46,13 +46,17 @@ jobs: with: token: ${{ secrets.SETUP_GLI_TOKEN }} + - name: Install 7zip + run: | + sudo apt update && sudo apt install -y 7zip + - name: Overwrite csc problem matcher run: echo "::add-matcher::.forgejo/csc.json" - name: Get version info id: version_info run: | - echo "result=$(gli get-next-version -c Stable -R)" >> $FORGEJO_OUTPUT + echo "result=$(gli get-next-version -c Canary -R)" >> $FORGEJO_OUTPUT echo "git_short_hash=$(git rev-parse --short "${{ forgejo.sha }}")" >> $FORGEJO_OUTPUT shell: bash @@ -83,14 +87,24 @@ jobs: run: dotnet publish -c "${{ matrix.configuration }}" -r "${{ matrix.platform.name }}" -o ./publish -p:Version="${{ steps.version_info.outputs.result }}" -p:DebugType=embedded -p:SourceRevisionId="${{ steps.version_info.outputs.git_short_hash }}" -p:ExtraDefineConstants=DISABLE_UPDATER src/Ryujinx --self-contained if: forgejo.event_name == 'pull_request' - - name: Set executable bit + - name: Packing Windows builds + if: contains(matrix.platform.name, 'win') run: | - chmod +x ./publish/Ryujinx ./publish/Ryujinx.sh - if: forgejo.event_name == 'pull_request' && contains(matrix.platform.name, 'linux') + 7z a artifact/ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-${{ matrix.platform.zip_os_name }}.7z publish + shell: bash + + - name: Upload Ryujinx Windows artifact + uses: actions/upload-artifact@v4 + with: + name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-${{ matrix.platform.zip_os_name }} + path: artifact + if: forgejo.event_name == 'pull_request' && contains(matrix.platform.name, 'win') - name: Build AppImage if: forgejo.event_name == 'pull_request' && contains(matrix.platform.name, 'linux') run: | + chmod +x ./publish/Ryujinx ./publish/Ryujinx.sh + PLATFORM_NAME="${{ matrix.platform.name }}" sudo apt update && sudo apt install -y zsync desktop-file-utils appstream libfuse2t64 @@ -118,14 +132,7 @@ jobs: BUILDDIR=publish OUTDIR=publish_appimage distribution/linux/appimage/build-appimage.sh shell: bash - - name: Upload Ryujinx artifact - uses: actions/upload-artifact@v4 - with: - name: ryujinx-${{ matrix.configuration }}-${{ steps.version_info.outputs.result }}+${{ steps.version_info.outputs.git_short_hash }}-${{ matrix.platform.zip_os_name }} - path: publish - if: forgejo.event_name == 'pull_request' - - - name: Upload Ryujinx (AppImage) artifact + - name: Upload Ryujinx AppImage artifact uses: actions/upload-artifact@v4 if: forgejo.event_name == 'pull_request' && contains(matrix.platform.name, 'linux') with: diff --git a/.forgejo/workflows/canary.yml b/.forgejo/workflows/canary.yml index 025dc2417..277d23c67 100644 --- a/.forgejo/workflows/canary.yml +++ b/.forgejo/workflows/canary.yml @@ -84,6 +84,7 @@ jobs: pushd publish rm libarmeilleure-jitsupport.dylib 7z a ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish + 7z a ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.7z ../publish popd shell: bash @@ -94,6 +95,7 @@ jobs: rm libarmeilleure-jitsupport.dylib chmod +x Ryujinx.sh Ryujinx tar -czvf ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz ../publish + tar -cJvf ../release_output/ryujinx-canary-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gx ../publish popd shell: bash diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index c6da033b1..45123c2cd 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -81,6 +81,7 @@ jobs: pushd publish rm libarmeilleure-jitsupport.dylib 7z a ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.zip ../publish + 7z a ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.7z ../publish popd shell: bash @@ -91,6 +92,7 @@ jobs: rm libarmeilleure-jitsupport.dylib chmod +x Ryujinx.sh Ryujinx tar -czvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.gz ../publish + tar -cJvf ../release_output/ryujinx-${{ steps.version_info.outputs.build_version }}-${{ matrix.platform.zip_os_name }}.tar.xz ../publish popd shell: bash diff --git a/Directory.Packages.props b/Directory.Packages.props index c17a8794d..5c7892bad 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,6 +8,7 @@ + @@ -43,12 +44,11 @@ - + - diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index ecbcc7927..926af7f50 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -46,6 +46,7 @@ + @@ -68,7 +69,6 @@ - diff --git a/src/Ryujinx/Systems/Updater/Updater.GitLab.cs b/src/Ryujinx/Systems/Updater/Updater.GitLab.cs index 73fe9f66b..1972e1422 100644 --- a/src/Ryujinx/Systems/Updater/Updater.GitLab.cs +++ b/src/Ryujinx/Systems/Updater/Updater.GitLab.cs @@ -91,7 +91,7 @@ namespace Ryujinx.Ava.Systems } // If build URL not found, assume no new update is available. - if (_versionResponse.ArtifactUrl is null or "") + if (string.IsNullOrEmpty(_versionResponse.ArtifactUrl)) { if (showVersionUpToDate) { @@ -123,6 +123,8 @@ namespace Ryujinx.Ava.Systems return default; } + _connectionCount = (int)_versionResponse.MaxConcurrency; + return (currentVersion, newVersion); } } diff --git a/src/Ryujinx/Systems/Updater/Updater.cs b/src/Ryujinx/Systems/Updater/Updater.cs index ac168eb03..e1b45e4b0 100644 --- a/src/Ryujinx/Systems/Updater/Updater.cs +++ b/src/Ryujinx/Systems/Updater/Updater.cs @@ -1,19 +1,20 @@ using Avalonia.Threading; using FluentAvalonia.UI.Controls; using Gommon; -using ICSharpCode.SharpZipLib.GZip; -using ICSharpCode.SharpZipLib.Tar; -using ICSharpCode.SharpZipLib.Zip; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.Utilities; using Ryujinx.Common; using Ryujinx.Common.Helper; using Ryujinx.Common.Logging; +using SharpCompress.Archives; +using SharpCompress.Compressors.Xz; using System; using System.Collections.Generic; using System.Diagnostics; +using System.Formats.Tar; using System.IO; +using System.IO.Compression; using System.Linq; using System.Net; using System.Net.Http; @@ -21,7 +22,6 @@ 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; @@ -32,8 +32,8 @@ namespace Ryujinx.Ava.Systems private static readonly string _homeDir = AppDomain.CurrentDomain.BaseDirectory; private static readonly string _updateDir = Path.Combine(Path.GetTempPath(), "Ryujinx", "update"); private static readonly string _updatePublishDir = Path.Combine(_updateDir, "publish"); - private const int ConnectionCount = 4; + private static int _connectionCount = 1; private static long _buildSize; private static bool _updateSuccessful; private static bool _running; @@ -73,27 +73,6 @@ namespace Ryujinx.Ava.Systems return; } - // Fetch build size information to learn chunk sizes. - using HttpClient buildSizeClient = ConstructHttpClient(); - try - { - buildSizeClient.DefaultRequestHeaders.Add("Range", "bytes=0-0"); - - // Forgejo instance is located in Ukraine. Connection times will vary across the world. - buildSizeClient.Timeout = TimeSpan.FromSeconds(10); - - HttpResponseMessage message = await buildSizeClient.GetAsync(new Uri(_versionResponse.ArtifactUrl), HttpCompletionOption.ResponseHeadersRead); - - _buildSize = message.Content.Headers.ContentRange.Length.Value; - } - catch (Exception ex) - { - Logger.Warning?.Print(LogClass.Application, ex.Message); - Logger.Warning?.Print(LogClass.Application, "Couldn't determine build size for update, using single-threaded updater"); - - _buildSize = -1; - } - await Dispatcher.UIThread.InvokeAsync(async () => { string newVersionString = ReleaseInformation.IsCanaryBuild @@ -143,6 +122,14 @@ namespace Ryujinx.Ava.Systems Directory.CreateDirectory(_updateDir); + // If we get a .zip url switch it to the preferred .7z file instead + // The update server still returns the .zip url by default for legacy support + downloadUrl = downloadUrl.Replace(".zip", ".7z"); + + // If we get a .tar.gz url switch it to the preferred .tar.xz file instead + // The update server still returns the .tar.gz url by default for legacy support + downloadUrl = downloadUrl.Replace(".tar.gz", ".tar.xz"); + string updateFile = Path.Combine(_updateDir, "update.bin"); TaskDialog taskDialog = new() @@ -153,6 +140,27 @@ namespace Ryujinx.Ava.Systems ShowProgressBar = true, XamlRoot = RyujinxApp.MainWindow, }; + + // Fetch build size information to learn chunk sizes. + using HttpClient buildSizeClient = ConstructHttpClient(); + try + { + buildSizeClient.DefaultRequestHeaders.Add("Range", "bytes=0-0"); + + // Forgejo instance is located in Ukraine. Connection times will vary across the world. + buildSizeClient.Timeout = TimeSpan.FromSeconds(10); + + HttpResponseMessage message = await buildSizeClient.GetAsync(new Uri(_versionResponse.ArtifactUrl), HttpCompletionOption.ResponseHeadersRead); + + _buildSize = message.Content.Headers.ContentRange.Length.Value; + } + catch (Exception ex) + { + Logger.Warning?.Print(LogClass.Application, ex.Message); + Logger.Warning?.Print(LogClass.Application, "Couldn't determine build size for update, using single-threaded updater"); + + _buildSize = -1; + } taskDialog.Opened += (s, e) => { @@ -234,22 +242,22 @@ namespace Ryujinx.Ava.Systems private static void DoUpdateWithMultipleThreads(TaskDialog taskDialog, string downloadUrl, string updateFile) { // Multi-Threaded Updater - long chunkSize = _buildSize / ConnectionCount; - long remainderChunk = _buildSize % ConnectionCount; + long chunkSize = _buildSize / _connectionCount; + long remainderChunk = _buildSize % _connectionCount; int completedRequests = 0; int totalProgressPercentage = 0; - int[] progressPercentage = new int[ConnectionCount]; + int[] progressPercentage = new int[_connectionCount]; - List list = new(ConnectionCount); - List webClients = new(ConnectionCount); + List list = new(_connectionCount); + List webClients = new(_connectionCount); - for (int i = 0; i < ConnectionCount; i++) + for (int i = 0; i < _connectionCount; i++) { list.Add([]); } - for (int i = 0; i < ConnectionCount; i++) + for (int i = 0; i < _connectionCount; i++) { #pragma warning disable SYSLIB0014 // TODO: WebClient is obsolete and need to be replaced with a more complex logic using HttpClient. @@ -258,7 +266,7 @@ namespace Ryujinx.Ava.Systems webClients.Add(client); - if (i == ConnectionCount - 1) + if (i == _connectionCount - 1) { client.Headers.Add("Range", $"bytes={chunkSize * i}-{(chunkSize * (i + 1) - 1) + remainderChunk}"); } @@ -275,7 +283,7 @@ namespace Ryujinx.Ava.Systems Interlocked.Exchange(ref progressPercentage[index], args.ProgressPercentage); Interlocked.Add(ref totalProgressPercentage, args.ProgressPercentage); - taskDialog.SetProgressBarState(totalProgressPercentage / ConnectionCount, TaskDialogProgressState.Normal); + taskDialog.SetProgressBarState(totalProgressPercentage / _connectionCount, TaskDialogProgressState.Normal); }; client.DownloadDataCompleted += (_, args) => @@ -294,10 +302,10 @@ namespace Ryujinx.Ava.Systems list[index] = args.Result; Interlocked.Increment(ref completedRequests); - if (Equals(completedRequests, ConnectionCount)) + if (Equals(completedRequests, _connectionCount)) { byte[] mergedFileBytes = new byte[_buildSize]; - for (int connectionIndex = 0, destinationOffset = 0; connectionIndex < ConnectionCount; connectionIndex++) + for (int connectionIndex = 0, destinationOffset = 0; connectionIndex < _connectionCount; connectionIndex++) { Array.Copy(list[connectionIndex], 0, mergedFileBytes, destinationOffset, list[connectionIndex].Length); destinationOffset += list[connectionIndex].Length; @@ -402,73 +410,33 @@ namespace Ryujinx.Ava.Systems [SupportedOSPlatform("linux")] [SupportedOSPlatform("macos")] - private static void ExtractTarGzipFile(TaskDialog taskDialog, string archivePath, string outputDirectoryPath) + private static void ExtractTarGzipFile(string archivePath, string outputDirectoryPath) { using FileStream inStream = File.OpenRead(archivePath); - using GZipInputStream gzipStream = new(inStream); - using TarInputStream tarStream = new(gzipStream, Encoding.ASCII); - - TarEntry tarEntry; - - while ((tarEntry = tarStream.GetNextEntry()) is not null) - { - if (tarEntry.IsDirectory) - { - continue; - } - - string outPath = Path.Combine(outputDirectoryPath, tarEntry.Name); - - Directory.CreateDirectory(Path.GetDirectoryName(outPath)); - - using FileStream outStream = File.OpenWrite(outPath); - tarStream.CopyEntryContents(outStream); - - File.SetUnixFileMode(outPath, (UnixFileMode)tarEntry.TarHeader.Mode); - File.SetLastWriteTime(outPath, DateTime.SpecifyKind(tarEntry.ModTime, DateTimeKind.Utc)); - - Dispatcher.UIThread.Post(() => - { - if (tarEntry is null) - { - return; - } - - taskDialog.SetProgressBarState(GetPercentage(tarEntry.Size, inStream.Length), TaskDialogProgressState.Normal); - }); - } + using GZipStream gzipStream = new(inStream, CompressionMode.Decompress); + + TarFile.ExtractToDirectory(gzipStream, outputDirectoryPath, true); + } + + [SupportedOSPlatform("linux")] + [SupportedOSPlatform("macos")] + private static void ExtractTarXzipFile(string archivePath, string outputDirectoryPath) + { + using FileStream inStream = File.OpenRead(archivePath); + using XZStream gzipStream = new(inStream); + + TarFile.ExtractToDirectory(gzipStream, outputDirectoryPath, true); } - private static void ExtractZipFile(TaskDialog taskDialog, string archivePath, string outputDirectoryPath) + private static void ExtractZipFile(string archivePath, string outputDirectoryPath) { - using Stream inStream = File.OpenRead(archivePath); - using ZipFile zipFile = new(inStream); - - double count = 0; - foreach (ZipEntry zipEntry in zipFile) - { - count++; - if (zipEntry.IsDirectory) - { - continue; - } - - string outPath = Path.Combine(outputDirectoryPath, zipEntry.Name); - - Directory.CreateDirectory(Path.GetDirectoryName(outPath)); - - using Stream zipStream = zipFile.GetInputStream(zipEntry); - using FileStream outStream = File.OpenWrite(outPath); - - zipStream.CopyTo(outStream); - - File.SetLastWriteTime(outPath, DateTime.SpecifyKind(zipEntry.DateTime, DateTimeKind.Utc)); - - Dispatcher.UIThread.Post(() => - { - taskDialog.SetProgressBarState(GetPercentage(count, zipFile.Count), TaskDialogProgressState.Normal); - }); - } + ZipFile.ExtractToDirectory(archivePath, outputDirectoryPath); + } + + private static void Extract7ZipFile(string archivePath, string outputDirectoryPath) + { + IArchive archive = ArchiveFactory.OpenArchive(archivePath); + archive.WriteToDirectory(outputDirectoryPath); } private static void InstallUpdate(TaskDialog taskDialog, string updateFile) @@ -479,16 +447,20 @@ namespace Ryujinx.Ava.Systems if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { - ExtractTarGzipFile(taskDialog, updateFile, _updateDir); + ExtractTarXzipFile(updateFile, _updateDir); } else if (OperatingSystem.IsWindows()) { - ExtractZipFile(taskDialog, updateFile, _updateDir); + Extract7ZipFile(updateFile, _updateDir); } else { throw new NotSupportedException(); } + + // The new decompression implementations don't have a way to show progress + // so the progressbar is just set to 100% after the decompression is done + taskDialog.SetProgressBarState(100, TaskDialogProgressState.Normal); // Delete downloaded zip File.Delete(updateFile);