diff --git a/StabilityMatrix.Avalonia/Assets.cs b/StabilityMatrix.Avalonia/Assets.cs index caf3cbc5..aec7ea14 100644 --- a/StabilityMatrix.Avalonia/Assets.cs +++ b/StabilityMatrix.Avalonia/Assets.cs @@ -3,6 +3,7 @@ using System.IO; using System.Runtime.Versioning; using System.Threading.Tasks; using Avalonia.Platform; +using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Core.Helper; namespace StabilityMatrix.Avalonia; @@ -17,39 +18,18 @@ internal static class Assets [SupportedOSPlatform("windows")] [SupportedOSPlatform("linux")] - public static Uri SevenZipExecutable - { - get - { - if (Compat.IsWindows) - { - return new Uri("avares://StabilityMatrix.Avalonia/Assets/win-x64/7za.exe"); - } - if (Compat.Platform.HasFlag(PlatformKind.Linux | PlatformKind.X64)) - { - return new Uri("avares://StabilityMatrix.Avalonia/Assets/linux-x64/7zzs"); - } - throw new PlatformNotSupportedException(); - } - } + public static AvaloniaResource SevenZipExecutable => Compat.Switch( + (PlatformKind.Windows, + new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/win-x64/7za.exe")), + (PlatformKind.Linux | PlatformKind.X64, + new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/linux-x64/7zzs", + UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute))); [SupportedOSPlatform("windows")] [SupportedOSPlatform("linux")] - public static Uri SevenZipLicense - { - get - { - if (Compat.IsWindows) - { - return new Uri("avares://StabilityMatrix.Avalonia/Assets/win-x64/7za - LICENSE.txt"); - } - if (Compat.Platform.HasFlag(PlatformKind.Linux | PlatformKind.X64)) - { - return new Uri("avares://StabilityMatrix.Avalonia/Assets/linux-x64/7zzs - LICENSE.txt"); - } - throw new PlatformNotSupportedException(); - } - } + public static AvaloniaResource SevenZipLicense => Compat.Switch( + (PlatformKind.Windows, new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/win-x64/7za - LICENSE.txt")), + (PlatformKind.Linux | PlatformKind.X64, new AvaloniaResource("avares://StabilityMatrix.Avalonia/Assets/linux-x64/7zzs - LICENSE.txt"))); [SupportedOSPlatform("windows")] [SupportedOSPlatform("linux")] diff --git a/StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs b/StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs index 9137da7f..3d3336e2 100644 --- a/StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs +++ b/StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs @@ -5,6 +5,7 @@ using System.Runtime.Versioning; using System.Threading.Tasks; using Avalonia; using Avalonia.Platform; +using Avalonia.Utilities; using NLog; using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Helper; @@ -57,11 +58,11 @@ public class UnixPrerequisiteHelper : IPrerequisiteHelper }; progress?.Report(new ProgressReport(0, message: "Unpacking resources", isIndeterminate: true)); - + Directory.CreateDirectory(AssetsDir); - foreach (var (assetUri, extractTo) in assets) + foreach (var (asset, extractDir) in assets) { - await Assets.ExtractAsset(assetUri, extractTo); + await asset.ExtractTo(extractDir); } progress?.Report(new ProgressReport(1, message: "Unpacking resources", isIndeterminate: false)); @@ -112,7 +113,7 @@ public class UnixPrerequisiteHelper : IPrerequisiteHelper await PythonDir.DeleteAsync(true); } progress?.Report(new ProgressReport(0, "Installing Python", isIndeterminate: true)); - await ArchiveHelper.ExtractManaged(downloadPath, PythonDir); + await ArchiveHelper.Extract7ZAuto(downloadPath, PythonDir); // For Linux, move the inner 'python' folder up to the root PythonDir if (Compat.IsLinux) diff --git a/StabilityMatrix.Avalonia/Models/AvaloniaResource.cs b/StabilityMatrix.Avalonia/Models/AvaloniaResource.cs new file mode 100644 index 00000000..25e85c82 --- /dev/null +++ b/StabilityMatrix.Avalonia/Models/AvaloniaResource.cs @@ -0,0 +1,41 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Threading.Tasks; +using Avalonia.Platform; +using StabilityMatrix.Core.Helper; + +namespace StabilityMatrix.Avalonia.Models; + +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public readonly record struct AvaloniaResource( + Uri UriPath, + UnixFileMode WriteUnixFileMode = UnixFileMode.None) +{ + public string FileName => UriPath.Segments[^1]; + + public AvaloniaResource(string uriPath, UnixFileMode writeUnixFileMode = UnixFileMode.None) + : this(new Uri(uriPath), writeUnixFileMode) + { + } + + /// + /// Extracts this resource to the output directory. + /// + public async Task ExtractTo(string outputDir, bool overwrite = true) + { + var targetPath = Path.Combine(outputDir, FileName); + if (File.Exists(targetPath) && !overwrite) + { + return; + } + var stream = AssetLoader.Open(UriPath); + await using var fileStream = File.Create(targetPath); + await stream.CopyToAsync(fileStream); + // Write permissions + if (!Compat.IsWindows && Compat.IsUnix && WriteUnixFileMode != UnixFileMode.None) + { + File.SetUnixFileMode(targetPath, WriteUnixFileMode); + } + } +} diff --git a/StabilityMatrix.Core/Helper/ArchiveHelper.cs b/StabilityMatrix.Core/Helper/ArchiveHelper.cs index 5f41ac79..d8612f8c 100644 --- a/StabilityMatrix.Core/Helper/ArchiveHelper.cs +++ b/StabilityMatrix.Core/Helper/ArchiveHelper.cs @@ -122,6 +122,58 @@ public static partial class ArchiveHelper var compressed = ulong.Parse(matches[1].Value); return new ArchiveInfo(size, compressed); } + + /// + /// Extracts a zipped tar (i.e. '.tar.gz') archive. + /// First extracts the zipped tar, then extracts the tar and removes the tar. + /// + /// + /// + /// + public static async Task Extract7ZTar(string archivePath, string extractDirectory) + { + if (!archivePath.EndsWith(".tar.gz")) + { + throw new ArgumentException("Archive must be a zipped tar."); + } + // Extract the tar.gz to tar + await Extract7Z(archivePath, extractDirectory); + + // Extract the tar + var tarPath = Path.Combine(extractDirectory, Path.GetFileNameWithoutExtension(archivePath) + ".tar"); + if (!File.Exists(tarPath)) + { + throw new FileNotFoundException("Tar file not found.", tarPath); + } + + try + { + return await Extract7Z(tarPath, extractDirectory); + } + finally + { + // Remove the tar + if (File.Exists(tarPath)) + { + File.Delete(tarPath); + } + } + } + + /// + /// Extracts with auto handling of tar.gz files. + /// + public static async Task Extract7ZAuto(string archivePath, string extractDirectory) + { + if (archivePath.EndsWith(".tar.gz")) + { + return await Extract7ZTar(archivePath, extractDirectory); + } + else + { + return await Extract7Z(archivePath, extractDirectory); + } + } /// /// Extract an archive to the output directory. @@ -212,6 +264,8 @@ public static partial class ArchiveHelper var entry = reader.Entry; var outputPath = Path.Combine(outputDirectory, entry.Key); + Logger.Debug($"Extracting key: {entry.Key.ToRepr()}"); + if (entry.IsDirectory) { if (!Directory.Exists(outputPath)) @@ -255,7 +309,7 @@ public static partial class ArchiveHelper // Delete path if exists File.Delete(outputPath); File.CreateSymbolicLink(outputPath, entry.LinkTarget); - return; + continue; } catch (IOException e) { diff --git a/StabilityMatrix.Core/Helper/Compat.cs b/StabilityMatrix.Core/Helper/Compat.cs index ad8bf5ad..f30f21a7 100644 --- a/StabilityMatrix.Core/Helper/Compat.cs +++ b/StabilityMatrix.Core/Helper/Compat.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using System.Runtime.Versioning; +using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Models.FileInterfaces; namespace StabilityMatrix.Core.Helper; @@ -104,6 +105,27 @@ public static class Compat AppData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); AppDataHome = AppData + AppName; } + + /// + /// Generic function to return different objects based on platform flags. + /// Parameters are checked in sequence with Compat.Platform.HasFlag, + /// the first match is returned. + /// + /// Thrown when no targets match + public static T Switch(params (PlatformKind platform, T target)[] targets) + { + foreach (var (platform, target) in targets) + { + if (Platform.HasFlag(platform)) + { + return target; + } + } + + throw new PlatformNotSupportedException( + $"Platform {Platform.ToString()} is not in supported targets: " + + $"{string.Join(", ", targets.Select(t => t.platform.ToString()))}"); + } /// /// Get the current application executable name.