|
|
|
@ -17,35 +17,42 @@ public class PrerequisiteHelper : IPrerequisiteHelper
|
|
|
|
|
private readonly IGitHubClient gitHubClient; |
|
|
|
|
private readonly IDownloadService downloadService; |
|
|
|
|
private readonly ISettingsManager settingsManager; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private const string VcRedistDownloadUrl = "https://aka.ms/vs/16/release/vc_redist.x64.exe"; |
|
|
|
|
private const string PythonDownloadUrl = "https://www.python.org/ftp/python/3.10.11/python-3.10.11-embed-amd64.zip"; |
|
|
|
|
private const string PythonDownloadHashBlake3 = "24923775f2e07392063aaa0c78fbd4ae0a320e1fc9c6cfbab63803402279fe5a"; |
|
|
|
|
|
|
|
|
|
private const string PythonDownloadUrl = |
|
|
|
|
"https://www.python.org/ftp/python/3.10.11/python-3.10.11-embed-amd64.zip"; |
|
|
|
|
private const string PythonDownloadHashBlake3 = |
|
|
|
|
"24923775f2e07392063aaa0c78fbd4ae0a320e1fc9c6cfbab63803402279fe5a"; |
|
|
|
|
|
|
|
|
|
private string HomeDir => settingsManager.LibraryDir; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private string VcRedistDownloadPath => Path.Combine(HomeDir, "vcredist.x64.exe"); |
|
|
|
|
|
|
|
|
|
private string AssetsDir => Path.Combine(HomeDir, "Assets"); |
|
|
|
|
private string SevenZipPath => Path.Combine(AssetsDir, "7za.exe"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private string PythonDownloadPath => Path.Combine(AssetsDir, "python-3.10.11-embed-amd64.zip"); |
|
|
|
|
private string PythonDir => Path.Combine(AssetsDir, "Python310"); |
|
|
|
|
private string PythonDllPath => Path.Combine(PythonDir, "python310.dll"); |
|
|
|
|
private string PythonLibraryZipPath => Path.Combine(PythonDir, "python310.zip"); |
|
|
|
|
private string GetPipPath => Path.Combine(PythonDir, "get-pip.pyc"); |
|
|
|
|
|
|
|
|
|
// Temporary directory to extract venv to during python install |
|
|
|
|
private string VenvTempDir => Path.Combine(PythonDir, "venv"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private string PortableGitInstallDir => Path.Combine(HomeDir, "PortableGit"); |
|
|
|
|
private string PortableGitDownloadPath => Path.Combine(HomeDir, "PortableGit.7z.exe"); |
|
|
|
|
private string GitExePath => Path.Combine(PortableGitInstallDir, "bin", "git.exe"); |
|
|
|
|
public string GitBinPath => Path.Combine(PortableGitInstallDir, "bin"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public bool IsPythonInstalled => File.Exists(PythonDllPath); |
|
|
|
|
|
|
|
|
|
public PrerequisiteHelper(ILogger<PrerequisiteHelper> logger, IGitHubClient gitHubClient, |
|
|
|
|
IDownloadService downloadService, ISettingsManager settingsManager) |
|
|
|
|
public PrerequisiteHelper( |
|
|
|
|
ILogger<PrerequisiteHelper> logger, |
|
|
|
|
IGitHubClient gitHubClient, |
|
|
|
|
IDownloadService downloadService, |
|
|
|
|
ISettingsManager settingsManager |
|
|
|
|
) |
|
|
|
|
{ |
|
|
|
|
this.logger = logger; |
|
|
|
|
this.gitHubClient = gitHubClient; |
|
|
|
@ -53,19 +60,33 @@ public class PrerequisiteHelper : IPrerequisiteHelper
|
|
|
|
|
this.settingsManager = settingsManager; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public async Task RunGit(string? workingDirectory = null, params string[] args) |
|
|
|
|
public async Task RunGit( |
|
|
|
|
string? workingDirectory = null, |
|
|
|
|
Action<ProcessOutput>? onProcessOutput = null, |
|
|
|
|
params string[] args |
|
|
|
|
) |
|
|
|
|
{ |
|
|
|
|
var process = ProcessRunner.StartAnsiProcess(GitExePath, args, workingDirectory: workingDirectory); |
|
|
|
|
var process = ProcessRunner.StartAnsiProcess( |
|
|
|
|
GitExePath, |
|
|
|
|
args, |
|
|
|
|
workingDirectory, |
|
|
|
|
onProcessOutput |
|
|
|
|
); |
|
|
|
|
await ProcessRunner.WaitForExitConditionAsync(process).ConfigureAwait(false); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public async Task<string> GetGitOutput(string? workingDirectory = null, params string[] args) |
|
|
|
|
{ |
|
|
|
|
var output = await ProcessRunner.GetProcessOutputAsync(GitExePath, string.Join(" ", args), |
|
|
|
|
workingDirectory: workingDirectory).ConfigureAwait(false); |
|
|
|
|
var output = await ProcessRunner |
|
|
|
|
.GetProcessOutputAsync( |
|
|
|
|
GitExePath, |
|
|
|
|
string.Join(" ", args), |
|
|
|
|
workingDirectory: workingDirectory |
|
|
|
|
) |
|
|
|
|
.ConfigureAwait(false); |
|
|
|
|
return output; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public async Task InstallAllIfNecessary(IProgress<ProgressReport>? progress = null) |
|
|
|
|
{ |
|
|
|
|
await InstallVcRedistIfNecessary(progress); |
|
|
|
@ -85,11 +106,14 @@ public class PrerequisiteHelper : IPrerequisiteHelper
|
|
|
|
|
// from "StabilityMatrix.Assets.Python310.libssl-1_1.dll" |
|
|
|
|
// to "Python310\libssl-1_1.dll" |
|
|
|
|
var fileExt = Path.GetExtension(resourceName); |
|
|
|
|
var fileName = resourceName |
|
|
|
|
.Replace(fileExt, "") |
|
|
|
|
.Replace("StabilityMatrix.Assets.", "") |
|
|
|
|
.Replace(".", Path.DirectorySeparatorChar.ToString()) + fileExt; |
|
|
|
|
await using var resourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName)!; |
|
|
|
|
var fileName = |
|
|
|
|
resourceName |
|
|
|
|
.Replace(fileExt, "") |
|
|
|
|
.Replace("StabilityMatrix.Assets.", "") |
|
|
|
|
.Replace(".", Path.DirectorySeparatorChar.ToString()) + fileExt; |
|
|
|
|
await using var resourceStream = Assembly |
|
|
|
|
.GetExecutingAssembly() |
|
|
|
|
.GetManifestResourceStream(resourceName)!; |
|
|
|
|
if (resourceStream == null) |
|
|
|
|
{ |
|
|
|
|
throw new Exception($"Resource {resourceName} not found"); |
|
|
|
@ -101,7 +125,11 @@ public class PrerequisiteHelper : IPrerequisiteHelper
|
|
|
|
|
/// <summary> |
|
|
|
|
/// Extracts all embedded resources starting with resourceDir to outputDirectory |
|
|
|
|
/// </summary> |
|
|
|
|
private async Task ExtractAllEmbeddedResources(string resourceDir, string outputDirectory, string resourceRoot = "StabilityMatrix.Assets.") |
|
|
|
|
private async Task ExtractAllEmbeddedResources( |
|
|
|
|
string resourceDir, |
|
|
|
|
string outputDirectory, |
|
|
|
|
string resourceRoot = "StabilityMatrix.Assets." |
|
|
|
|
) |
|
|
|
|
{ |
|
|
|
|
Directory.CreateDirectory(outputDirectory); |
|
|
|
|
// Unpack from embedded resources |
|
|
|
@ -117,12 +145,15 @@ public class PrerequisiteHelper : IPrerequisiteHelper
|
|
|
|
|
// from "StabilityMatrix.Assets.Python310.libssl-1_1.dll" |
|
|
|
|
// to "Python310\libssl-1_1.dll" |
|
|
|
|
var fileExt = Path.GetExtension(resourceName); |
|
|
|
|
var fileName = resourceName |
|
|
|
|
.Replace(fileExt, "") |
|
|
|
|
.Replace(resourceRoot, "") |
|
|
|
|
.Replace(".", Path.DirectorySeparatorChar.ToString()) + fileExt; |
|
|
|
|
var fileName = |
|
|
|
|
resourceName |
|
|
|
|
.Replace(fileExt, "") |
|
|
|
|
.Replace(resourceRoot, "") |
|
|
|
|
.Replace(".", Path.DirectorySeparatorChar.ToString()) + fileExt; |
|
|
|
|
// Unpack resource |
|
|
|
|
await using var resourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName)!; |
|
|
|
|
await using var resourceStream = Assembly |
|
|
|
|
.GetExecutingAssembly() |
|
|
|
|
.GetManifestResourceStream(resourceName)!; |
|
|
|
|
var outputFilePath = Path.Combine(outputDirectory, fileName); |
|
|
|
|
// Create missing directories |
|
|
|
|
var outputDir = Path.GetDirectoryName(outputFilePath); |
|
|
|
@ -134,13 +165,21 @@ public class PrerequisiteHelper : IPrerequisiteHelper
|
|
|
|
|
await resourceStream.CopyToAsync(fileStream); |
|
|
|
|
copied.Add(outputFilePath); |
|
|
|
|
} |
|
|
|
|
logger.LogInformation("Successfully unpacked {Num} embedded resources: [{Resources}]", total, string.Join(",", copied)); |
|
|
|
|
logger.LogInformation( |
|
|
|
|
"Successfully unpacked {Num} embedded resources: [{Resources}]", |
|
|
|
|
total, |
|
|
|
|
string.Join(",", copied) |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public async Task UnpackResourcesIfNecessary(IProgress<ProgressReport>? progress = null) |
|
|
|
|
{ |
|
|
|
|
// Skip if all files exist |
|
|
|
|
if (File.Exists(SevenZipPath) && File.Exists(PythonDllPath) && File.Exists(PythonLibraryZipPath)) |
|
|
|
|
if ( |
|
|
|
|
File.Exists(SevenZipPath) |
|
|
|
|
&& File.Exists(PythonDllPath) |
|
|
|
|
&& File.Exists(PythonLibraryZipPath) |
|
|
|
|
) |
|
|
|
|
{ |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
@ -149,14 +188,14 @@ public class PrerequisiteHelper : IPrerequisiteHelper
|
|
|
|
|
// Create directories |
|
|
|
|
Directory.CreateDirectory(AssetsDir); |
|
|
|
|
Directory.CreateDirectory(PythonDir); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Run if 7za missing |
|
|
|
|
if (!File.Exists(SevenZipPath)) |
|
|
|
|
{ |
|
|
|
|
await ExtractEmbeddedResource("StabilityMatrix.Assets.7za.exe", AssetsDir); |
|
|
|
|
await ExtractEmbeddedResource("StabilityMatrix.Assets.7za - LICENSE.txt", AssetsDir); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
progress?.Report(new ProgressReport(1f, "Unpacking complete")); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -171,60 +210,70 @@ public class PrerequisiteHelper : IPrerequisiteHelper
|
|
|
|
|
logger.LogInformation("Python not found at {PythonDllPath}, downloading...", PythonDllPath); |
|
|
|
|
|
|
|
|
|
Directory.CreateDirectory(AssetsDir); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Delete existing python zip if it exists |
|
|
|
|
if (File.Exists(PythonLibraryZipPath)) |
|
|
|
|
{ |
|
|
|
|
File.Delete(PythonLibraryZipPath); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger.LogInformation( |
|
|
|
|
"Downloading Python from {PythonLibraryZipUrl} to {PythonLibraryZipPath}", |
|
|
|
|
PythonDownloadUrl, PythonLibraryZipPath); |
|
|
|
|
|
|
|
|
|
"Downloading Python from {PythonLibraryZipUrl} to {PythonLibraryZipPath}", |
|
|
|
|
PythonDownloadUrl, |
|
|
|
|
PythonLibraryZipPath |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
// Cleanup to remove zip if download fails |
|
|
|
|
try |
|
|
|
|
{ |
|
|
|
|
// Download python zip |
|
|
|
|
await downloadService.DownloadToFileAsync(PythonDownloadUrl, PythonDownloadPath, progress: progress); |
|
|
|
|
|
|
|
|
|
await downloadService.DownloadToFileAsync( |
|
|
|
|
PythonDownloadUrl, |
|
|
|
|
PythonDownloadPath, |
|
|
|
|
progress: progress |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
// Verify python hash |
|
|
|
|
var downloadHash = await FileHash.GetBlake3Async(PythonDownloadPath, progress); |
|
|
|
|
if (downloadHash != PythonDownloadHashBlake3) |
|
|
|
|
{ |
|
|
|
|
var fileExists = File.Exists(PythonDownloadPath); |
|
|
|
|
var fileSize = new FileInfo(PythonDownloadPath).Length; |
|
|
|
|
var msg = $"Python download hash mismatch: {downloadHash} != {PythonDownloadHashBlake3} " + |
|
|
|
|
$"(file exists: {fileExists}, size: {fileSize})"; |
|
|
|
|
var msg = |
|
|
|
|
$"Python download hash mismatch: {downloadHash} != {PythonDownloadHashBlake3} " |
|
|
|
|
+ $"(file exists: {fileExists}, size: {fileSize})"; |
|
|
|
|
throw new Exception(msg); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
progress?.Report(new ProgressReport(progress: 1f, message: "Python download complete")); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
progress?.Report(new ProgressReport(-1, "Installing Python...", isIndeterminate: true)); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// We also need 7z if it's not already unpacked |
|
|
|
|
if (!File.Exists(SevenZipPath)) |
|
|
|
|
{ |
|
|
|
|
await ExtractEmbeddedResource("StabilityMatrix.Assets.7za.exe", AssetsDir); |
|
|
|
|
await ExtractEmbeddedResource("StabilityMatrix.Assets.7za - LICENSE.txt", AssetsDir); |
|
|
|
|
await ExtractEmbeddedResource( |
|
|
|
|
"StabilityMatrix.Assets.7za - LICENSE.txt", |
|
|
|
|
AssetsDir |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Delete existing python dir |
|
|
|
|
if (Directory.Exists(PythonDir)) |
|
|
|
|
{ |
|
|
|
|
Directory.Delete(PythonDir, true); |
|
|
|
|
} |
|
|
|
|
// Unzip python |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await ArchiveHelper.Extract7Z(PythonDownloadPath, PythonDir); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try |
|
|
|
|
{ |
|
|
|
|
// Extract embedded venv |
|
|
|
|
await ExtractAllEmbeddedResources("StabilityMatrix.Assets.venv", PythonDir); |
|
|
|
|
// Add venv to python's library zip |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await ArchiveHelper.AddToArchive7Z(PythonLibraryZipPath, VenvTempDir); |
|
|
|
|
} |
|
|
|
|
finally |
|
|
|
@ -235,16 +284,16 @@ public class PrerequisiteHelper : IPrerequisiteHelper
|
|
|
|
|
Directory.Delete(VenvTempDir, true); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Extract get-pip.pyc |
|
|
|
|
await ExtractEmbeddedResource("StabilityMatrix.Assets.get-pip.pyc", PythonDir); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// We need to uncomment the #import site line in python310._pth for pip to work |
|
|
|
|
var pythonPthPath = Path.Combine(PythonDir, "python310._pth"); |
|
|
|
|
var pythonPthContent = await File.ReadAllTextAsync(pythonPthPath); |
|
|
|
|
pythonPthContent = pythonPthContent.Replace("#import site", "import site"); |
|
|
|
|
await File.WriteAllTextAsync(pythonPthPath, pythonPthContent); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
progress?.Report(new ProgressReport(1f, "Python install complete")); |
|
|
|
|
} |
|
|
|
|
finally |
|
|
|
@ -264,7 +313,7 @@ public class PrerequisiteHelper : IPrerequisiteHelper
|
|
|
|
|
logger.LogDebug("Git already installed at {GitExePath}", GitExePath); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger.LogInformation("Git not found at {GitExePath}, downloading...", GitExePath); |
|
|
|
|
|
|
|
|
|
var portableGitUrl = |
|
|
|
@ -272,7 +321,11 @@ public class PrerequisiteHelper : IPrerequisiteHelper
|
|
|
|
|
|
|
|
|
|
if (!File.Exists(PortableGitDownloadPath)) |
|
|
|
|
{ |
|
|
|
|
await downloadService.DownloadToFileAsync(portableGitUrl, PortableGitDownloadPath, progress: progress); |
|
|
|
|
await downloadService.DownloadToFileAsync( |
|
|
|
|
portableGitUrl, |
|
|
|
|
PortableGitDownloadPath, |
|
|
|
|
progress: progress |
|
|
|
|
); |
|
|
|
|
progress?.Report(new ProgressReport(progress: 1f, message: "Git download complete")); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -284,7 +337,9 @@ public class PrerequisiteHelper : IPrerequisiteHelper
|
|
|
|
|
{ |
|
|
|
|
var registry = Registry.LocalMachine; |
|
|
|
|
var key = registry.OpenSubKey( |
|
|
|
|
@"SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\X64", false); |
|
|
|
|
@"SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\X64", |
|
|
|
|
false |
|
|
|
|
); |
|
|
|
|
if (key != null) |
|
|
|
|
{ |
|
|
|
|
var buildId = Convert.ToUInt32(key.GetValue("Bld")); |
|
|
|
@ -293,20 +348,44 @@ public class PrerequisiteHelper : IPrerequisiteHelper
|
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger.LogInformation("Downloading VC Redist"); |
|
|
|
|
|
|
|
|
|
await downloadService.DownloadToFileAsync(VcRedistDownloadUrl, VcRedistDownloadPath, progress: progress); |
|
|
|
|
progress?.Report(new ProgressReport(progress: 1f, message: "Visual C++ download complete", |
|
|
|
|
type: ProgressType.Download)); |
|
|
|
|
|
|
|
|
|
await downloadService.DownloadToFileAsync( |
|
|
|
|
VcRedistDownloadUrl, |
|
|
|
|
VcRedistDownloadPath, |
|
|
|
|
progress: progress |
|
|
|
|
); |
|
|
|
|
progress?.Report( |
|
|
|
|
new ProgressReport( |
|
|
|
|
progress: 1f, |
|
|
|
|
message: "Visual C++ download complete", |
|
|
|
|
type: ProgressType.Download |
|
|
|
|
) |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
logger.LogInformation("Installing VC Redist"); |
|
|
|
|
progress?.Report(new ProgressReport(progress: 0.5f, isIndeterminate: true, type: ProgressType.Generic, message: "Installing prerequisites...")); |
|
|
|
|
var process = ProcessRunner.StartAnsiProcess(VcRedistDownloadPath, "/install /quiet /norestart"); |
|
|
|
|
progress?.Report( |
|
|
|
|
new ProgressReport( |
|
|
|
|
progress: 0.5f, |
|
|
|
|
isIndeterminate: true, |
|
|
|
|
type: ProgressType.Generic, |
|
|
|
|
message: "Installing prerequisites..." |
|
|
|
|
) |
|
|
|
|
); |
|
|
|
|
var process = ProcessRunner.StartAnsiProcess( |
|
|
|
|
VcRedistDownloadPath, |
|
|
|
|
"/install /quiet /norestart" |
|
|
|
|
); |
|
|
|
|
await process.WaitForExitAsync(); |
|
|
|
|
progress?.Report(new ProgressReport(progress: 1f, message: "Visual C++ install complete", |
|
|
|
|
type: ProgressType.Generic)); |
|
|
|
|
|
|
|
|
|
progress?.Report( |
|
|
|
|
new ProgressReport( |
|
|
|
|
progress: 1f, |
|
|
|
|
message: "Visual C++ install complete", |
|
|
|
|
type: ProgressType.Generic |
|
|
|
|
) |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
File.Delete(VcRedistDownloadPath); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -335,5 +414,4 @@ public class PrerequisiteHelper : IPrerequisiteHelper
|
|
|
|
|
settingsManager.AddPathExtension(GitBinPath); |
|
|
|
|
settingsManager.InsertPathExtensions(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|