Browse Source

Compatibility for PyRunner and Prereq helper on linux

pull/55/head
Ionite 1 year ago
parent
commit
78c0dcabd3
No known key found for this signature in database
  1. 6
      StabilityMatrix.Avalonia/App.axaml.cs
  2. 88
      StabilityMatrix.Avalonia/Assets.cs
  3. BIN
      StabilityMatrix.Avalonia/Assets/linux-x64/7zzs
  4. 88
      StabilityMatrix.Avalonia/Assets/linux-x64/7zzs - LICENSE.txt
  5. 43
      StabilityMatrix.Avalonia/Assets/win-x64/7za - LICENSE.txt
  6. BIN
      StabilityMatrix.Avalonia/Assets/win-x64/7za.exe
  7. 155
      StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs
  8. 39
      StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs
  9. 23
      StabilityMatrix.Avalonia/Views/SettingsPage.axaml
  10. 54
      StabilityMatrix.Core/Helper/ArchiveHelper.cs
  11. 46
      StabilityMatrix.Core/Helper/Compat.cs
  12. 4
      StabilityMatrix.Core/Helper/HardwareHelper.cs
  13. 7
      StabilityMatrix.Core/Helper/IPrerequisiteHelper.cs
  14. 2
      StabilityMatrix.Core/Helper/PlatformKind.cs
  15. 51
      StabilityMatrix.Core/Python/PyRunner.cs

6
StabilityMatrix.Avalonia/App.axaml.cs

@ -27,6 +27,7 @@ using Polly.Timeout;
using Refit; using Refit;
using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.DesignData; using StabilityMatrix.Avalonia.DesignData;
using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
@ -222,6 +223,11 @@ public sealed class App : Application
services.AddSingleton<IPyRunner, PyRunner>(); services.AddSingleton<IPyRunner, PyRunner>();
services.AddSingleton<IUpdateHelper, UpdateHelper>(); services.AddSingleton<IUpdateHelper, UpdateHelper>();
if (Compat.IsLinux)
{
services.AddSingleton<IPrerequisiteHelper, UnixPrerequisiteHelper>();
}
ConfigureViews(services); ConfigureViews(services);
if (Design.IsDesignMode) if (Design.IsDesignMode)

88
StabilityMatrix.Avalonia/Assets.cs

@ -1,4 +1,9 @@
using System; using System;
using System.IO;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Avalonia.Platform;
using StabilityMatrix.Core.Helper;
namespace StabilityMatrix.Avalonia; namespace StabilityMatrix.Avalonia;
@ -9,10 +14,93 @@ internal static class Assets
/// </summary> /// </summary>
public static Uri NoImage { get; } = public static Uri NoImage { get; } =
new("avares://StabilityMatrix.Avalonia/Assets/noimage.png"); new("avares://StabilityMatrix.Avalonia/Assets/noimage.png");
[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-linux-x64");
}
throw new PlatformNotSupportedException();
}
}
[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();
}
}
[SupportedOSPlatform("windows")]
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
public static (Uri url, string hashSha256) PythonDownloadUrl
{
get
{
if (Compat.IsWindows)
{
return (new Uri("https://www.python.org/ftp/python/3.10.11/python-3.10.11-embed-amd64.zip"),
"608619f8619075629c9c69f361352a0da6ed7e62f83a0e19c63e0ea32eb7629d");
}
if (Compat.Platform.HasFlag(PlatformKind.Linux | PlatformKind.X64))
{
return (new Uri("https://github.com/indygreg/python-build-standalone/releases/download/" +
"20230507/cpython-3.10.11+20230507-x86_64-unknown-linux-gnu-install_only.tar.gz"),
"c5bcaac91bc80bfc29cf510669ecad12d506035ecb3ad85ef213416d54aecd79");
}
if (Compat.Platform.HasFlag(PlatformKind.MacOS | PlatformKind.Arm))
{
return (new Uri("https://github.com/indygreg/python-build-standalone/releases/download/" +
"20230507/cpython-3.10.11+20230507-aarch64-apple-darwin-install_only.tar.gz"),
"8348bc3c2311f94ec63751fb71bd0108174be1c4def002773cf519ee1506f96f");
}
throw new PlatformNotSupportedException();
}
}
public static Uri DiscordServerUrl { get; } = public static Uri DiscordServerUrl { get; } =
new("https://discord.com/invite/TUrgfECxHz"); new("https://discord.com/invite/TUrgfECxHz");
public static Uri PatreonUrl { get; } = public static Uri PatreonUrl { get; } =
new("https://patreon.com/StabilityMatrix"); new("https://patreon.com/StabilityMatrix");
/// <summary>
/// Extracts an asset URI to a target directory.
/// </summary>
public static async Task ExtractAsset(Uri assetUri, string targetDirectory, bool overwrite = true)
{
var assetPath = assetUri.AbsolutePath;
var assetName = Path.GetFileName(assetPath);
var targetPath = Path.Combine(targetDirectory, assetName);
if (File.Exists(targetPath) && !overwrite)
{
return;
}
var stream = AssetLoader.Open(assetUri);
await using var fileStream = File.Create(targetPath);
await stream.CopyToAsync(fileStream);
}
} }

BIN
StabilityMatrix.Avalonia/Assets/linux-x64/7zzs

Binary file not shown.

88
StabilityMatrix.Avalonia/Assets/linux-x64/7zzs - LICENSE.txt

@ -0,0 +1,88 @@
7-Zip
~~~~~
License for use and distribution
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
7-Zip Copyright (C) 1999-2023 Igor Pavlov.
The licenses for 7zz and 7zzs files are:
- The "GNU LGPL" as main license for most of the code
- The "GNU LGPL" with "unRAR license restriction" for some code
- The "BSD 3-clause License" for some code
Redistributions in binary form must reproduce related license information from this file.
Note:
You can use 7-Zip on any computer, including a computer in a commercial
organization. You don't need to register or pay for 7-Zip.
GNU LGPL information
--------------------
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You can receive a copy of the GNU Lesser General Public License from
http://www.gnu.org/
BSD 3-clause License
--------------------
The "BSD 3-clause License" is used for the code in 7z.dll that implements LZFSE data decompression.
That code was derived from the code in the "LZFSE compression library" developed by Apple Inc,
that also uses the "BSD 3-clause License":
----
Copyright (c) 2015-2016, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
----
unRAR license restriction
-------------------------
The decompression engine for RAR archives was developed using source
code of unRAR program.
All copyrights to original unRAR code are owned by Alexander Roshal.
The license for original unRAR code has the following restriction:
The unRAR sources cannot be used to re-create the RAR compression algorithm,
which is proprietary. Distribution of modified unRAR sources in separate form
or as a part of other software is permitted, provided that it is clearly
stated in the documentation and source comments that the code may
not be used to develop a RAR (WinRAR) compatible archiver.
--
Igor Pavlov

43
StabilityMatrix.Avalonia/Assets/win-x64/7za - LICENSE.txt

@ -0,0 +1,43 @@
7-Zip Extra 18.01
-----------------
7-Zip Extra is package of extra modules of 7-Zip.
7-Zip Copyright (C) 1999-2018 Igor Pavlov.
7-Zip is free software. Read License.txt for more information about license.
Source code of binaries can be found at:
http://www.7-zip.org/
7-Zip Extra
~~~~~~~~~~~
License for use and distribution
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Copyright (C) 1999-2018 Igor Pavlov.
7-Zip Extra files are under the GNU LGPL license.
Notes:
You can use 7-Zip Extra on any computer, including a computer in a commercial
organization. You don't need to register or pay for 7-Zip.
GNU LGPL information
--------------------
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You can receive a copy of the GNU Lesser General Public License from
http://www.gnu.org/

BIN
StabilityMatrix.Avalonia/Assets/win-x64/7za.exe

Binary file not shown.

155
StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs

@ -0,0 +1,155 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Platform;
using NLog;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.Helpers;
[SupportedOSPlatform("linux")]
public class UnixPrerequisiteHelper : IPrerequisiteHelper
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private readonly IDownloadService downloadService;
private readonly ISettingsManager settingsManager;
private string HomeDir => settingsManager.LibraryDir;
private string AssetsDir => Path.Combine(HomeDir, "Assets");
private string PythonDir => Path.Combine(AssetsDir, "Python310");
private string PythonDllPath => Path.Combine(PythonDir, "python310.dll");
public bool IsPythonInstalled => File.Exists(PythonDllPath);
private string PortableGitInstallDir => Path.Combine(HomeDir, "PortableGit");
public string GitBinPath => Path.Combine(PortableGitInstallDir, "bin");
public UnixPrerequisiteHelper(IDownloadService downloadService, ISettingsManager settingsManager)
{
this.downloadService = downloadService;
this.settingsManager = settingsManager;
}
public async Task InstallAllIfNecessary(IProgress<ProgressReport>? progress = null)
{
await UnpackResourcesIfNecessary(progress);
await InstallGitIfNecessary(progress);
await InstallPythonIfNecessary(progress);
}
public async Task UnpackResourcesIfNecessary(IProgress<ProgressReport>? progress = null)
{
// Array of (asset_uri, extract_to)
var assets = new[]
{
(Assets.SevenZipExecutable, AssetsDir),
(Assets.SevenZipLicense, AssetsDir),
};
progress?.Report(new ProgressReport(0, message: "Unpacking resources", isIndeterminate: true));
foreach (var (assetUri, extractTo) in assets)
{
await Assets.ExtractAsset(assetUri, extractTo);
}
progress?.Report(new ProgressReport(1, message: "Unpacking resources", isIndeterminate: false));
}
public Task InstallGitIfNecessary(IProgress<ProgressReport>? progress = null)
{
return Task.CompletedTask;
}
public async Task RunGit(string? workingDirectory = null, params string[] args)
{
var result = await ProcessRunner.RunBashCommand("git" + args, workingDirectory ?? "");
if (result.ExitCode != 0)
{
throw new ProcessException($"Git command failed with exit code {result.ExitCode}:\n" +
$"{result.StandardOutput}\n{result.StandardError}");
}
}
public async Task InstallPythonIfNecessary(IProgress<ProgressReport>? progress = null)
{
if (IsPythonInstalled) return;
Directory.CreateDirectory(AssetsDir);
// Download
var (url, hashSha256) = Assets.PythonDownloadUrl;
var fileName = Path.GetFileName(url.LocalPath);
var downloadPath = Path.Combine(AssetsDir, fileName);
Logger.Info($"Downloading Python from {url.AbsolutePath} to {downloadPath}");
try
{
await downloadService.DownloadToFileAsync(url.AbsolutePath, downloadPath, progress);
// Verify hash
var actualHash = await FileHash.GetSha256Async(downloadPath);
Logger.Info($"Verifying Python hash: (expected: {hashSha256}, actual: {actualHash})");
if (actualHash != hashSha256)
{
throw new Exception($"Python download hash mismatch: expected {hashSha256}, actual {actualHash}");
}
// Extract
Logger.Info($"Extracting Python Zip: {downloadPath} to {PythonDir}");
Directory.Delete(PythonDir, true);
if (progress != null)
{
await ArchiveHelper.Extract7Z(downloadPath, PythonDir, progress);
}
else
{
await ArchiveHelper.Extract7Z(downloadPath, PythonDir);
}
// For Linux, move the inner 'python' folder up to the root PythonDir
if (Compat.IsLinux)
{
var innerPythonDir = Path.Combine(PythonDir, "python");
foreach (var folder in Directory.EnumerateDirectories(innerPythonDir))
{
var folderName = Path.GetFileName(folder);
var dest = Path.Combine(PythonDir, folderName);
Directory.Move(folder, dest);
}
Directory.Delete(innerPythonDir);
}
}
finally
{
// Cleanup download file
File.Delete(downloadPath);
}
}
public Task SetupPythonDependencies(string installLocation, string requirementsFileName,
IProgress<ProgressReport>? progress = null, Action<ProcessOutput>? onConsoleOutput = null)
{
throw new NotImplementedException();
}
public void UpdatePathExtensions()
{
throw new NotImplementedException();
}
[UnsupportedOSPlatform("Linux")]
[UnsupportedOSPlatform("macOS")]
public Task InstallVcRedistIfNecessary(IProgress<ProgressReport>? progress = null)
{
throw new PlatformNotSupportedException();
}
}

39
StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs

@ -14,6 +14,7 @@ using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using Symbol = FluentIcons.Common.Symbol; using Symbol = FluentIcons.Common.Symbol;
using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource; using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
@ -25,6 +26,8 @@ public partial class SettingsViewModel : PageViewModelBase
{ {
private readonly INotificationService notificationService; private readonly INotificationService notificationService;
private readonly ISettingsManager settingsManager; private readonly ISettingsManager settingsManager;
private readonly IPrerequisiteHelper prerequisiteHelper;
private readonly IPyRunner pyRunner;
public override string Title => "Settings"; public override string Title => "Settings";
public override IconSource IconSource => new SymbolIconSource {Symbol = Symbol.Settings, IsFilled = true}; public override IconSource IconSource => new SymbolIconSource {Symbol = Symbol.Settings, IsFilled = true};
@ -39,11 +42,16 @@ public partial class SettingsViewModel : PageViewModelBase
"System", "System",
}; };
public SettingsViewModel(INotificationService notificationService, public SettingsViewModel(
ISettingsManager settingsManager) INotificationService notificationService,
ISettingsManager settingsManager,
IPrerequisiteHelper prerequisiteHelper,
IPyRunner pyRunner)
{ {
this.notificationService = notificationService; this.notificationService = notificationService;
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.prerequisiteHelper = prerequisiteHelper;
this.pyRunner = pyRunner;
SelectedTheme = AvailableThemes[1]; SelectedTheme = AvailableThemes[1];
} }
@ -132,4 +140,31 @@ public partial class SettingsViewModel : PageViewModelBase
notificationService.Show(new Notification("Content dialog closed", notificationService.Show(new Notification("Content dialog closed",
$"Result: {result}")); $"Result: {result}"));
} }
public async Task ShowPythonVersion()
{
// Ensure python installed
if (!prerequisiteHelper.IsPythonInstalled)
{
// Need 7z as well for site packages repack
await prerequisiteHelper.UnpackResourcesIfNecessary();
await prerequisiteHelper.InstallPythonIfNecessary();
}
// Get python version
await pyRunner.Initialize();
var result = await pyRunner.GetVersionInfo();
// Show dialog box
var dialog = new ContentDialog
{
Title = "Python version info",
Content = result,
PrimaryButtonText = "Ok",
IsPrimaryButtonEnabled = true
};
dialog.Title = "Python version info";
dialog.Content = result;
dialog.PrimaryButtonText = "Ok";
await dialog.ShowAsync();
}
} }

23
StabilityMatrix.Avalonia/Views/SettingsPage.axaml

@ -6,6 +6,7 @@
xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:icons="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="vm:SettingsViewModel" x:DataType="vm:SettingsViewModel"
x:CompileBindings="True" x:CompileBindings="True"
@ -27,9 +28,9 @@
</ui:SettingsExpander> </ui:SettingsExpander>
<!-- TODO: Text2Image host port settings --> <!-- TODO: Text2Image host port settings -->
<!-- TODO: Keep folder links on shutdown --> <!-- TODO: Keep folder links on shutdown -->
<!-- Checkpoints Manager Options -->
<Grid RowDefinitions="auto,*"> <Grid RowDefinitions="auto,*">
<TextBlock <TextBlock
FontWeight="Medium" FontWeight="Medium"
@ -48,6 +49,26 @@
</StackPanel> </StackPanel>
</Grid> </Grid>
<!-- Python Options -->
<Grid RowDefinitions="auto,*">
<TextBlock
FontWeight="Medium"
Text="Python Environment"
Margin="8,16,0,0" />
<StackPanel Grid.Row="1">
<ui:SettingsExpander
Header="Embedded Python Environment"
Margin="8">
<ui:SettingsExpander.IconSource>
<controls:FASymbolIconSource Symbol="fa-brands fa-python"/>
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<Button Content="Check Version"/>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</StackPanel>
</Grid>
<Grid RowDefinitions="auto,*"> <Grid RowDefinitions="auto,*">
<TextBlock <TextBlock
FontWeight="Medium" FontWeight="Medium"

54
StabilityMatrix.Core/Helper/ArchiveHelper.cs

@ -1,9 +1,11 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using NLog; using NLog;
using SharpCompress.Common; using SharpCompress.Common;
using SharpCompress.Compressors.Deflate;
using SharpCompress.Readers; using SharpCompress.Readers;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Processes;
@ -14,16 +16,34 @@ namespace StabilityMatrix.Core.Helper;
public record struct ArchiveInfo(ulong Size, ulong CompressedSize); public record struct ArchiveInfo(ulong Size, ulong CompressedSize);
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public static class ArchiveHelper public static class ArchiveHelper
{ {
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
/// <summary>
/// Platform-specific 7z executable name.
/// </summary>
public static string SevenZipFileName
{
get
{
if (Compat.IsWindows)
{
return "7za.exe";
}
if (Compat.Platform.HasFlag(PlatformKind.Linux))
{
return "7zzs";
}
throw new PlatformNotSupportedException("7z is not supported on this platform.");
}
}
// HomeDir is set by ISettingsManager.TryFindLibrary() // HomeDir is set by ISettingsManager.TryFindLibrary()
public static string HomeDir { get; set; } = string.Empty; public static string HomeDir { get; set; } = string.Empty;
public static string SevenZipPath => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) public static string SevenZipPath => Path.Combine(HomeDir, "Assets", SevenZipFileName);
? Path.Combine(HomeDir, "Assets", "7za.exe")
: throw new NotImplementedException("need to implement 7z path for non-windows");
private static readonly Regex Regex7ZOutput = new(@"(?<=Size:\s*)\d+|(?<=Compressed:\s*)\d+"); private static readonly Regex Regex7ZOutput = new(@"(?<=Size:\s*)\d+|(?<=Compressed:\s*)\d+");
private static readonly Regex Regex7ZProgressDigits = new(@"(?<=\s*)\d+(?=%)"); private static readonly Regex Regex7ZProgressDigits = new(@"(?<=\s*)\d+(?=%)");
@ -39,7 +59,7 @@ public static class ArchiveHelper
var compressed = ulong.Parse(matches[1].Value); var compressed = ulong.Parse(matches[1].Value);
return new ArchiveInfo(size, compressed); return new ArchiveInfo(size, compressed);
} }
public static async Task AddToArchive7Z(string archivePath, string sourceDirectory) public static async Task AddToArchive7Z(string archivePath, string sourceDirectory)
{ {
// Start 7z in the parent directory of the source directory // Start 7z in the parent directory of the source directory
@ -165,4 +185,30 @@ public static class ArchiveHelper
progressMonitor?.Stop(); progressMonitor?.Stop();
Logger.Info("Finished extracting archive {}", archivePath); Logger.Info("Finished extracting archive {}", archivePath);
} }
/// <summary>
/// Extract an archive to the output directory, using SharpCompress managed code.
/// does not require 7z to be installed, but no progress reporting.
/// </summary>
/// <param name="archivePath"></param>
/// <param name="outputDirectory">Output directory, created if does not exist.</param>
public static async Task ExtractManaged(string archivePath, string outputDirectory)
{
await using var stream = File.OpenRead(archivePath);
using var reader = ReaderFactory.Open(stream);
while (reader.MoveToNextEntry())
{
var entry = reader.Entry;
if (entry.IsDirectory)
{
Directory.CreateDirectory(Path.Combine(outputDirectory, entry.Key));
}
else
{
await using var entryStream = reader.OpenEntryStream();
await using var fileStream = File.Create(Path.Combine(outputDirectory, entry.Key));
await entryStream.CopyToAsync(fileStream);
}
}
}
} }

46
StabilityMatrix.Core/Helper/Compat.cs

@ -17,15 +17,18 @@ public static class Compat
// OS Platform // OS Platform
public static PlatformKind Platform { get; } public static PlatformKind Platform { get; }
[SupportedOSPlatformGuard("Windows")] [SupportedOSPlatformGuard("windows")]
public static bool IsWindows => Platform.HasFlag(PlatformKind.Windows); public static bool IsWindows => Platform.HasFlag(PlatformKind.Windows);
[SupportedOSPlatformGuard("Linux")] [SupportedOSPlatformGuard("linux")]
public static bool IsLinux => Platform.HasFlag(PlatformKind.Linux); public static bool IsLinux => Platform.HasFlag(PlatformKind.Linux);
[SupportedOSPlatformGuard("macOS")] [SupportedOSPlatformGuard("macos")]
public static bool IsMacOS => Platform.HasFlag(PlatformKind.MacOS); public static bool IsMacOS => Platform.HasFlag(PlatformKind.MacOS);
public static bool IsUnix => Platform.HasFlag(PlatformKind.Unix); public static bool IsUnix => Platform.HasFlag(PlatformKind.Unix);
public static bool IsArm => Platform.HasFlag(PlatformKind.Arm);
public static bool IsX64 => Platform.HasFlag(PlatformKind.X64);
// Paths // Paths
@ -44,18 +47,36 @@ public static class Compat
/// </summary> /// </summary>
public static DirectoryPath AppCurrentDir { get; } public static DirectoryPath AppCurrentDir { get; }
// File extensions
/// <summary>
/// Platform-specific executable extension.
/// ".exe" on Windows, Empty string on Linux and MacOS.
/// </summary>
public static string ExeExtension { get; }
/// <summary>
/// Platform-specific dynamic library extension.
/// ".dll" on Windows, ".dylib" on MacOS, ".so" on Linux.
/// </summary>
public static string DllExtension { get; }
static Compat() static Compat()
{ {
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
Platform = PlatformKind.Windows; Platform = PlatformKind.Windows;
AppCurrentDir = AppContext.BaseDirectory; AppCurrentDir = AppContext.BaseDirectory;
ExeExtension = ".exe";
DllExtension = ".dll";
} }
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{ {
Platform = PlatformKind.MacOS | PlatformKind.Unix; Platform = PlatformKind.MacOS | PlatformKind.Unix;
AppCurrentDir = AppContext.BaseDirectory; // TODO: check this
ExeExtension = "";
DllExtension = ".dylib";
} }
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{ {
Platform = PlatformKind.Linux | PlatformKind.Unix; Platform = PlatformKind.Linux | PlatformKind.Unix;
// We need to get application path using `$APPIMAGE`, then get the directory name // We need to get application path using `$APPIMAGE`, then get the directory name
@ -63,6 +84,21 @@ public static class Compat
throw new Exception("Could not find application path"); throw new Exception("Could not find application path");
AppCurrentDir = Path.GetDirectoryName(appPath) ?? AppCurrentDir = Path.GetDirectoryName(appPath) ??
throw new Exception("Could not find application directory"); throw new Exception("Could not find application directory");
ExeExtension = "";
DllExtension = ".so";
}
else
{
throw new PlatformNotSupportedException();
}
if (RuntimeInformation.ProcessArchitecture == Architecture.Arm)
{
Platform |= PlatformKind.Arm;
}
else
{
Platform |= PlatformKind.X64;
} }
AppData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); AppData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);

4
StabilityMatrix.Core/Helper/HardwareHelper.cs

@ -26,7 +26,7 @@ public static partial class HardwareHelper
return output; return output;
} }
[SupportedOSPlatform("Windows")] [SupportedOSPlatform("windows")]
private static IEnumerable<GpuInfo> IterGpuInfoWindows() private static IEnumerable<GpuInfo> IterGpuInfoWindows()
{ {
const string gpuRegistryKeyPath = const string gpuRegistryKeyPath =
@ -50,7 +50,7 @@ public static partial class HardwareHelper
} }
} }
[SupportedOSPlatform("Linux")] [SupportedOSPlatform("linux")]
private static IEnumerable<GpuInfo> IterGpuInfoLinux() private static IEnumerable<GpuInfo> IterGpuInfoLinux()
{ {
var output = RunBashCommand("lspci | grep VGA"); var output = RunBashCommand("lspci | grep VGA");

7
StabilityMatrix.Core/Helper/IPrerequisiteHelper.cs

@ -1,4 +1,5 @@
using StabilityMatrix.Core.Models.Progress; using System.Runtime.Versioning;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Processes;
namespace StabilityMatrix.Core.Helper; namespace StabilityMatrix.Core.Helper;
@ -12,8 +13,10 @@ public interface IPrerequisiteHelper
Task InstallAllIfNecessary(IProgress<ProgressReport>? progress = null); Task InstallAllIfNecessary(IProgress<ProgressReport>? progress = null);
Task UnpackResourcesIfNecessary(IProgress<ProgressReport>? progress = null); Task UnpackResourcesIfNecessary(IProgress<ProgressReport>? progress = null);
Task InstallGitIfNecessary(IProgress<ProgressReport>? progress = null); Task InstallGitIfNecessary(IProgress<ProgressReport>? progress = null);
Task InstallVcRedistIfNecessary(IProgress<ProgressReport>? progress = null);
Task InstallPythonIfNecessary(IProgress<ProgressReport>? progress = null); Task InstallPythonIfNecessary(IProgress<ProgressReport>? progress = null);
[SupportedOSPlatform("Windows")]
Task InstallVcRedistIfNecessary(IProgress<ProgressReport>? progress = null);
/// <summary> /// <summary>
/// Run embedded git with the given arguments. /// Run embedded git with the given arguments.

2
StabilityMatrix.Core/Helper/PlatformKind.cs

@ -8,4 +8,6 @@ public enum PlatformKind
Unix = 1 << 1, Unix = 1 << 1,
Linux = Unix | 1 << 2, Linux = Unix | 1 << 2,
MacOS = Unix | 1 << 3, MacOS = Unix | 1 << 3,
Arm = 1 << 20,
X64 = 1 << 21,
} }

51
StabilityMatrix.Core/Python/PyRunner.cs

@ -1,25 +1,35 @@
using NLog; using System.Diagnostics.CodeAnalysis;
using NLog;
using Python.Runtime; using Python.Runtime;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Python.Interop; using StabilityMatrix.Core.Python.Interop;
namespace StabilityMatrix.Core.Python; namespace StabilityMatrix.Core.Python;
[SuppressMessage("ReSharper", "NotAccessedPositionalProperty.Global")]
public record struct PyVersionInfo(int Major, int Minor, int Micro, string ReleaseLevel, int Serial); public record struct PyVersionInfo(int Major, int Minor, int Micro, string ReleaseLevel, int Serial);
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public class PyRunner : IPyRunner public class PyRunner : IPyRunner
{ {
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
// Set by ISettingsManager.TryFindLibrary() // Set by ISettingsManager.TryFindLibrary()
public static string HomeDir { get; set; } = string.Empty; public static DirectoryPath HomeDir { get; set; } = new();
// This is same for all platforms
public const string PythonDirName = "Python310";
public static string PythonDir => Path.Combine(HomeDir, "Assets", PythonDirName);
public static string PythonDllPath { get; }
public static string PythonExePath { get; }
public static string PipExePath { get; }
public static string PythonDir => Path.Combine(HomeDir, "Assets", "Python310");
public static string PythonDllPath => Path.Combine(PythonDir, "python310.dll");
public static string PythonExePath => Path.Combine(PythonDir, "python.exe");
public static string GetPipPath => Path.Combine(PythonDir, "get-pip.pyc"); public static string GetPipPath => Path.Combine(PythonDir, "get-pip.pyc");
public static string PipExePath => Path.Combine(PythonDir, "Scripts", "pip.exe"); // public static string PipExePath => Path.Combine(PythonDir, "Scripts", "pip" + Compat.ExeExtension);
public static string VenvPath => Path.Combine(PythonDir, "Scripts", "virtualenv.exe"); public static string VenvPath => Path.Combine(PythonDir, "Scripts", "virtualenv" + Compat.ExeExtension);
public static bool PipInstalled => File.Exists(PipExePath); public static bool PipInstalled => File.Exists(PipExePath);
public static bool VenvInstalled => File.Exists(VenvPath); public static bool VenvInstalled => File.Exists(VenvPath);
@ -29,7 +39,28 @@ public class PyRunner : IPyRunner
public PyIOStream? StdOutStream; public PyIOStream? StdOutStream;
public PyIOStream? StdErrStream; public PyIOStream? StdErrStream;
/// <summary> // Initialize paths based on platform
static PyRunner()
{
if (Compat.IsWindows)
{
PythonDllPath = Path.Combine(PythonDir, "python310.dll");
PythonExePath = Path.Combine(PythonDir, "python.exe");
PipExePath = Path.Combine(PythonDir, "Scripts", "pip.exe");
}
else if (Compat.IsLinux)
{
PythonDllPath = Path.Combine(PythonDir, "lib", "libpython3.10.so");
PythonExePath = Path.Combine(PythonDir, "bin", "python3.10");
PipExePath = Path.Combine(PythonDir, "bin", "pip3.10");
}
else
{
throw new PlatformNotSupportedException();
}
}
/// <summary>$
/// Initializes the Python runtime using the embedded dll. /// Initializes the Python runtime using the embedded dll.
/// Can be called with no effect after initialization. /// Can be called with no effect after initialization.
/// </summary> /// </summary>
@ -49,8 +80,8 @@ public class PyRunner : IPyRunner
// Check PythonDLL exists // Check PythonDLL exists
if (!File.Exists(PythonDllPath)) if (!File.Exists(PythonDllPath))
{ {
Logger.Error("Python DLL not found"); Logger.Error("Python linked library not found");
throw new FileNotFoundException("Python DLL not found", PythonDllPath); throw new FileNotFoundException("Python linked library not found", PythonDllPath);
} }
Runtime.PythonDLL = PythonDllPath; Runtime.PythonDLL = PythonDllPath;

Loading…
Cancel
Save