You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
486 lines
16 KiB
486 lines
16 KiB
using System; |
|
using System.Collections.Generic; |
|
using System.ComponentModel; |
|
using System.Diagnostics; |
|
using System.IO; |
|
using System.Linq; |
|
using System.Runtime.Versioning; |
|
using System.Threading.Tasks; |
|
using Avalonia.Controls; |
|
using DynamicData; |
|
using FluentAvalonia.UI.Controls; |
|
using NLog; |
|
using StabilityMatrix.Avalonia.Languages; |
|
using StabilityMatrix.Core.Exceptions; |
|
using StabilityMatrix.Core.Helper; |
|
using StabilityMatrix.Core.Models; |
|
using StabilityMatrix.Core.Models.FileInterfaces; |
|
using StabilityMatrix.Core.Models.Packages; |
|
using StabilityMatrix.Core.Models.Progress; |
|
using StabilityMatrix.Core.Processes; |
|
using StabilityMatrix.Core.Python; |
|
using StabilityMatrix.Core.Services; |
|
|
|
namespace StabilityMatrix.Avalonia.Helpers; |
|
|
|
[SupportedOSPlatform("macos")] |
|
[SupportedOSPlatform("linux")] |
|
public class UnixPrerequisiteHelper( |
|
IDownloadService downloadService, |
|
ISettingsManager settingsManager, |
|
IPyRunner pyRunner |
|
) : IPrerequisiteHelper |
|
{ |
|
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); |
|
|
|
private DirectoryPath HomeDir => settingsManager.LibraryDir; |
|
private DirectoryPath AssetsDir => HomeDir.JoinDir("Assets"); |
|
|
|
private DirectoryPath PythonDir => AssetsDir.JoinDir("Python310"); |
|
public bool IsPythonInstalled => PythonDir.JoinFile(PyRunner.RelativePythonDllPath).Exists; |
|
private DirectoryPath PortableGitInstallDir => HomeDir + "PortableGit"; |
|
public string GitBinPath => PortableGitInstallDir + "bin"; |
|
|
|
private DirectoryPath NodeDir => AssetsDir.JoinDir("nodejs"); |
|
private string NpmPath => Path.Combine(NodeDir, "bin", "npm"); |
|
private bool IsNodeInstalled => File.Exists(NpmPath); |
|
|
|
private DirectoryPath DotnetDir => AssetsDir.JoinDir("dotnet"); |
|
private string DotnetPath => Path.Combine(DotnetDir, "dotnet"); |
|
private bool IsDotnetInstalled => File.Exists(DotnetPath); |
|
private string Dotnet7DownloadUrlMacOs => |
|
"https://download.visualstudio.microsoft.com/download/pr/5bb0e0e4-2a8d-4aba-88ad-232e1f65c281/ee6d35f762d81965b4cf336edde1b318/dotnet-sdk-7.0.405-osx-arm64.tar.gz"; |
|
private string Dotnet8DownloadUrlMacOs => |
|
"https://download.visualstudio.microsoft.com/download/pr/ef083c06-7aee-4a4f-b18b-50c9a8990753/e206864e7910e81bbd9cb7e674ff1b4c/dotnet-sdk-8.0.101-osx-arm64.tar.gz"; |
|
private string Dotnet7DownloadUrlLinux => |
|
"https://download.visualstudio.microsoft.com/download/pr/5202b091-2406-445c-b40a-68a5b97c882b/b509f2a7a0eb61aea145b990b40b6d5b/dotnet-sdk-7.0.405-linux-x64.tar.gz"; |
|
private string Dotnet8DownloadUrlLinux => |
|
"https://download.visualstudio.microsoft.com/download/pr/9454f7dc-b98e-4a64-a96d-4eb08c7b6e66/da76f9c6bc4276332b587b771243ae34/dotnet-sdk-8.0.101-linux-x64.tar.gz"; |
|
|
|
// Cached store of whether or not git is installed |
|
private bool? isGitInstalled; |
|
|
|
private async Task<bool> CheckIsGitInstalled() |
|
{ |
|
var result = await ProcessRunner.RunBashCommand("git --version"); |
|
isGitInstalled = result.ExitCode == 0; |
|
return isGitInstalled == true; |
|
} |
|
|
|
public Task InstallPackageRequirements(BasePackage package, IProgress<ProgressReport>? progress = null) => |
|
InstallPackageRequirements(package.Prerequisites.ToList(), progress); |
|
|
|
public async Task InstallPackageRequirements( |
|
List<PackagePrerequisite> prerequisites, |
|
IProgress<ProgressReport>? progress = null |
|
) |
|
{ |
|
await UnpackResourcesIfNecessary(progress); |
|
|
|
if (prerequisites.Contains(PackagePrerequisite.Python310)) |
|
{ |
|
await InstallPythonIfNecessary(progress); |
|
await InstallVirtualenvIfNecessary(progress); |
|
} |
|
|
|
if (prerequisites.Contains(PackagePrerequisite.Git)) |
|
{ |
|
await InstallGitIfNecessary(progress); |
|
} |
|
|
|
if (prerequisites.Contains(PackagePrerequisite.Node)) |
|
{ |
|
await InstallNodeIfNecessary(progress); |
|
} |
|
|
|
if (prerequisites.Contains(PackagePrerequisite.Dotnet)) |
|
{ |
|
await InstallDotnetIfNecessary(progress); |
|
} |
|
} |
|
|
|
public async Task InstallDotnetIfNecessary(IProgress<ProgressReport>? progress = null) |
|
{ |
|
if (IsDotnetInstalled) |
|
return; |
|
|
|
if (Compat.IsMacOS) |
|
{ |
|
await DownloadAndExtractPrerequisite(progress, Dotnet7DownloadUrlMacOs, DotnetDir); |
|
await DownloadAndExtractPrerequisite(progress, Dotnet8DownloadUrlMacOs, DotnetDir); |
|
} |
|
else |
|
{ |
|
await DownloadAndExtractPrerequisite(progress, Dotnet7DownloadUrlLinux, DotnetDir); |
|
await DownloadAndExtractPrerequisite(progress, Dotnet8DownloadUrlLinux, DotnetDir); |
|
} |
|
} |
|
|
|
private async Task InstallVirtualenvIfNecessary(IProgress<ProgressReport>? progress = null) |
|
{ |
|
// python stuff |
|
if (!PyRunner.PipInstalled || !PyRunner.VenvInstalled) |
|
{ |
|
progress?.Report( |
|
new ProgressReport(-1f, "Installing Python prerequisites...", isIndeterminate: true) |
|
); |
|
|
|
await pyRunner.Initialize().ConfigureAwait(false); |
|
|
|
if (!PyRunner.PipInstalled) |
|
{ |
|
await pyRunner.SetupPip().ConfigureAwait(false); |
|
} |
|
if (!PyRunner.VenvInstalled) |
|
{ |
|
await pyRunner.InstallPackage("virtualenv").ConfigureAwait(false); |
|
} |
|
} |
|
} |
|
|
|
public async Task InstallAllIfNecessary(IProgress<ProgressReport>? progress = null) |
|
{ |
|
await UnpackResourcesIfNecessary(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)); |
|
|
|
Directory.CreateDirectory(AssetsDir); |
|
foreach (var (asset, extractDir) in assets) |
|
{ |
|
await asset.ExtractToDir(extractDir); |
|
} |
|
|
|
progress?.Report(new ProgressReport(1, message: "Unpacking resources", isIndeterminate: false)); |
|
} |
|
|
|
public async Task InstallGitIfNecessary(IProgress<ProgressReport>? progress = null) |
|
{ |
|
if (isGitInstalled == true || (isGitInstalled == null && await CheckIsGitInstalled())) |
|
return; |
|
|
|
// Show prompt to install git |
|
var dialog = new ContentDialog |
|
{ |
|
Title = "Git not found", |
|
Content = new StackPanel |
|
{ |
|
Children = |
|
{ |
|
new TextBlock |
|
{ |
|
Text = "The current operation requires Git. Please install it to continue." |
|
}, |
|
new SelectableTextBlock { Text = "$ sudo apt install git" }, |
|
} |
|
}, |
|
PrimaryButtonText = Resources.Action_Retry, |
|
CloseButtonText = Resources.Action_Close, |
|
DefaultButton = ContentDialogButton.Primary, |
|
}; |
|
|
|
while (true) |
|
{ |
|
// Return if installed |
|
if (await CheckIsGitInstalled()) |
|
return; |
|
if (await dialog.ShowAsync() == ContentDialogResult.None) |
|
{ |
|
// Cancel |
|
throw new OperationCanceledException("Git installation canceled"); |
|
} |
|
// Otherwise continue to retry indefinitely |
|
} |
|
} |
|
|
|
/// <inheritdoc /> |
|
public Task RunGit( |
|
ProcessArgs args, |
|
Action<ProcessOutput>? onProcessOutput = null, |
|
string? workingDirectory = null |
|
) |
|
{ |
|
// Async progress not supported on Unix |
|
return RunGit(args, workingDirectory); |
|
} |
|
|
|
/// <inheritdoc /> |
|
public async Task RunGit(ProcessArgs args, string? workingDirectory = null) |
|
{ |
|
var command = args.Prepend("git"); |
|
|
|
var result = await ProcessRunner.RunBashCommand(command.ToArray(), workingDirectory ?? ""); |
|
if (result.ExitCode != 0) |
|
{ |
|
Logger.Error( |
|
"Git command [{Command}] failed with exit code " + "{ExitCode}:\n{StdOut}\n{StdErr}", |
|
command, |
|
result.ExitCode, |
|
result.StandardOutput, |
|
result.StandardError |
|
); |
|
|
|
throw new ProcessException( |
|
$"Git command [{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 remote = Assets.PythonDownloadUrl; |
|
var url = remote.Url; |
|
var hashSha256 = remote.HashSha256; |
|
|
|
var fileName = Path.GetFileName(url.LocalPath); |
|
var downloadPath = Path.Combine(AssetsDir, fileName); |
|
Logger.Info($"Downloading Python from {url} to {downloadPath}"); |
|
try |
|
{ |
|
await downloadService.DownloadToFileAsync(url.ToString(), 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}"); |
|
if (PythonDir.Exists) |
|
{ |
|
await PythonDir.DeleteAsync(true); |
|
} |
|
progress?.Report(new ProgressReport(0, "Installing Python", isIndeterminate: true)); |
|
await ArchiveHelper.Extract7ZAuto(downloadPath, PythonDir); |
|
|
|
// For Unix, move the inner 'python' folder up to the root PythonDir |
|
if (Compat.IsUnix) |
|
{ |
|
var innerPythonDir = PythonDir.JoinDir("python"); |
|
if (!innerPythonDir.Exists) |
|
{ |
|
throw new Exception( |
|
$"Python download did not contain expected inner 'python' folder: {innerPythonDir}" |
|
); |
|
} |
|
|
|
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 |
|
if (File.Exists(downloadPath)) |
|
{ |
|
File.Delete(downloadPath); |
|
} |
|
} |
|
|
|
// Initialize pyrunner and install virtualenv |
|
await pyRunner.Initialize(); |
|
await pyRunner.InstallPackage("virtualenv"); |
|
|
|
progress?.Report(new ProgressReport(1, "Installing Python", isIndeterminate: false)); |
|
} |
|
|
|
public Task<ProcessResult> GetGitOutput(ProcessArgs args, string? workingDirectory = null) |
|
{ |
|
return ProcessRunner.RunBashCommand(args.Prepend("git").ToArray(), workingDirectory ?? ""); |
|
} |
|
|
|
[Localizable(false)] |
|
[SupportedOSPlatform("Linux")] |
|
[SupportedOSPlatform("macOS")] |
|
public async Task RunNpm( |
|
ProcessArgs args, |
|
string? workingDirectory = null, |
|
Action<ProcessOutput>? onProcessOutput = null, |
|
IReadOnlyDictionary<string, string>? envVars = null |
|
) |
|
{ |
|
var process = ProcessRunner.StartAnsiProcess( |
|
NpmPath, |
|
args, |
|
workingDirectory, |
|
onProcessOutput, |
|
envVars |
|
); |
|
|
|
await process.WaitForExitAsync(); |
|
|
|
if (process.ExitCode == 0) |
|
return; |
|
|
|
var stdOut = await process.StandardOutput.ReadToEndAsync(); |
|
var stdErr = await process.StandardError.ReadToEndAsync(); |
|
|
|
Logger.Error( |
|
"RunNpm with args [{Args}] failed with exit code " + "{ExitCode}:\n{StdOut}\n{StdErr}", |
|
args, |
|
process.ExitCode, |
|
stdOut, |
|
stdErr |
|
); |
|
|
|
throw new ProcessException( |
|
$"RunNpm with args [{args}] failed with exit code" + $" {process.ExitCode}:\n{stdOut}\n{stdErr}" |
|
); |
|
} |
|
|
|
[SupportedOSPlatform("Linux")] |
|
[SupportedOSPlatform("macOS")] |
|
public async Task<Process> RunDotnet( |
|
ProcessArgs args, |
|
string? workingDirectory = null, |
|
Action<ProcessOutput>? onProcessOutput = null, |
|
IReadOnlyDictionary<string, string>? envVars = null, |
|
bool waitForExit = true |
|
) |
|
{ |
|
var process = ProcessRunner.StartAnsiProcess( |
|
DotnetPath, |
|
args, |
|
workingDirectory, |
|
onProcessOutput, |
|
envVars |
|
); |
|
|
|
if (!waitForExit) |
|
return process; |
|
|
|
await process.WaitForExitAsync(); |
|
|
|
if (process.ExitCode == 0) |
|
return process; |
|
|
|
Logger.Error( |
|
"dotnet8 with args [{Args}] failed with exit code " + "{ExitCode}:\n{StdOut}\n{StdErr}", |
|
args, |
|
process.ExitCode, |
|
process.StandardOutput, |
|
process.StandardError |
|
); |
|
|
|
throw new ProcessException( |
|
$"dotnet8 with args [{args}] failed with exit code" |
|
+ $" {process.ExitCode}:\n{process.StandardOutput}\n{process.StandardError}" |
|
); |
|
} |
|
|
|
[SupportedOSPlatform("Linux")] |
|
[SupportedOSPlatform("macOS")] |
|
public async Task InstallNodeIfNecessary(IProgress<ProgressReport>? progress = null) |
|
{ |
|
if (IsNodeInstalled) |
|
{ |
|
Logger.Info("node already installed"); |
|
return; |
|
} |
|
|
|
Logger.Info("Downloading node"); |
|
|
|
var downloadUrl = Compat.IsMacOS |
|
? "https://nodejs.org/dist/v20.11.0/node-v20.11.0-darwin-arm64.tar.gz" |
|
: "https://nodejs.org/dist/v20.11.0/node-v20.11.0-linux-x64.tar.gz"; |
|
|
|
var nodeDownloadPath = AssetsDir.JoinFile(Path.GetFileName(downloadUrl)); |
|
|
|
await downloadService.DownloadToFileAsync(downloadUrl, nodeDownloadPath, progress: progress); |
|
|
|
Logger.Info("Installing node"); |
|
progress?.Report( |
|
new ProgressReport( |
|
progress: 0.5f, |
|
isIndeterminate: true, |
|
type: ProgressType.Generic, |
|
message: "Installing prerequisites..." |
|
) |
|
); |
|
|
|
// unzip |
|
await ArchiveHelper.Extract7ZAuto(nodeDownloadPath, AssetsDir); |
|
|
|
var nodeDir = Compat.IsMacOS |
|
? AssetsDir.JoinDir("node-v20.11.0-darwin-arm64") |
|
: AssetsDir.JoinDir("node-v20.11.0-linux-x64"); |
|
Directory.Move(nodeDir, NodeDir); |
|
|
|
progress?.Report( |
|
new ProgressReport(progress: 1f, message: "Node install complete", type: ProgressType.Generic) |
|
); |
|
|
|
File.Delete(nodeDownloadPath); |
|
} |
|
|
|
private async Task DownloadAndExtractPrerequisite( |
|
IProgress<ProgressReport>? progress, |
|
string downloadUrl, |
|
string extractPath |
|
) |
|
{ |
|
Logger.Info($"Downloading {downloadUrl}"); |
|
|
|
var downloadPath = AssetsDir.JoinFile(Path.GetFileName(downloadUrl)); |
|
|
|
await downloadService.DownloadToFileAsync(downloadUrl, downloadPath, progress: progress); |
|
|
|
Logger.Info("Installing prereq"); |
|
progress?.Report( |
|
new ProgressReport( |
|
progress: 0.5f, |
|
isIndeterminate: true, |
|
type: ProgressType.Generic, |
|
message: "Installing prerequisites..." |
|
) |
|
); |
|
|
|
Directory.CreateDirectory(extractPath); |
|
|
|
// unzip |
|
await ArchiveHelper.Extract7ZAuto(downloadPath, extractPath); |
|
|
|
progress?.Report( |
|
new ProgressReport(progress: 1f, message: "Node install complete", type: ProgressType.Generic) |
|
); |
|
|
|
File.Delete(downloadPath); |
|
} |
|
|
|
[UnsupportedOSPlatform("Linux")] |
|
[UnsupportedOSPlatform("macOS")] |
|
public Task InstallTkinterIfNecessary(IProgress<ProgressReport>? progress = null) |
|
{ |
|
throw new PlatformNotSupportedException(); |
|
} |
|
|
|
[UnsupportedOSPlatform("Linux")] |
|
[UnsupportedOSPlatform("macOS")] |
|
public Task InstallVcRedistIfNecessary(IProgress<ProgressReport>? progress = null) |
|
{ |
|
throw new PlatformNotSupportedException(); |
|
} |
|
}
|
|
|