Browse Source

Add extension management backends

pull/438/head
ionite34 11 months ago
parent
commit
137eca4950
No known key found for this signature in database
GPG Key ID: B3404C5F3827849B
  1. 2
      StabilityMatrix.Avalonia/ViewModels/ExtensionViewModel.cs
  2. 88
      StabilityMatrix.Core/Helper/IPrerequisiteHelper.cs
  3. 33
      StabilityMatrix.Core/Helper/PrerequisiteHelper.cs
  4. 13
      StabilityMatrix.Core/Models/GitVersion.cs
  5. 19
      StabilityMatrix.Core/Models/PackageModification/InstallExtensionStep.cs
  6. 14
      StabilityMatrix.Core/Models/Packages/BasePackage.cs
  7. 36
      StabilityMatrix.Core/Models/Packages/ComfyUI.cs
  8. 43
      StabilityMatrix.Core/Models/Packages/Extensions/ComfyExtensionManifest.cs
  9. 24
      StabilityMatrix.Core/Models/Packages/Extensions/ExtensionBase.cs
  10. 3
      StabilityMatrix.Core/Models/Packages/Extensions/ExtensionManifest.cs
  11. 144
      StabilityMatrix.Core/Models/Packages/Extensions/GitPackageExtensionManager.cs
  12. 37
      StabilityMatrix.Core/Models/Packages/Extensions/IPackageExtensionManager.cs
  13. 21
      StabilityMatrix.Core/Models/Packages/Extensions/InstalledPackageExtension.cs
  14. 16
      StabilityMatrix.Core/Models/Packages/Extensions/PackageExtension.cs
  15. 3
      StabilityMatrix.Core/Models/Packages/Extensions/PackageExtensionVersion.cs

2
StabilityMatrix.Avalonia/ViewModels/ExtensionViewModel.cs

@ -9,5 +9,5 @@ public partial class ExtensionViewModel() : ViewModelBase
[ObservableProperty] [ObservableProperty]
private bool isSelected; private bool isSelected;
public ExtensionBase Extension { get; init; } public PackageExtension PackageExtension { get; init; }
} }

88
StabilityMatrix.Core/Helper/IPrerequisiteHelper.cs

@ -1,4 +1,5 @@
using System.Runtime.Versioning; using System.Runtime.Versioning;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Processes;
@ -30,13 +31,96 @@ public interface IPrerequisiteHelper
Task<ProcessResult> GetGitOutput(ProcessArgs args, string? workingDirectory = null); Task<ProcessResult> GetGitOutput(ProcessArgs args, string? workingDirectory = null);
async Task<bool> CheckIsGitRepository(string directory) async Task<bool> CheckIsGitRepository(string repositoryPath)
{ {
var result = await GetGitOutput(["rev-parse", "--is-inside-work-tree"], directory) var result = await GetGitOutput(["rev-parse", "--is-inside-work-tree"], repositoryPath)
.ConfigureAwait(false); .ConfigureAwait(false);
return result.ExitCode == 0 && result.StandardOutput?.Trim().ToLowerInvariant() == "true"; return result.ExitCode == 0 && result.StandardOutput?.Trim().ToLowerInvariant() == "true";
} }
async Task<GitVersion> GetGitRepositoryVersion(string repositoryPath)
{
var version = new GitVersion();
// Get tag
if (
await GetGitOutput(["describe", "--tags", "--abbrev=0"], repositoryPath).ConfigureAwait(false) is
{ IsSuccessExitCode: true } tagResult
)
{
version = version with { Tag = tagResult.StandardOutput?.Trim() };
}
// Get branch
if (
await GetGitOutput(["rev-parse", "--abbrev-ref", "HEAD"], repositoryPath).ConfigureAwait(false) is
{ IsSuccessExitCode: true } branchResult
)
{
version = version with { Branch = branchResult.StandardOutput?.Trim() };
}
// Get commit sha
if (
await GetGitOutput(["rev-parse", "HEAD"], repositoryPath).ConfigureAwait(false) is
{ IsSuccessExitCode: true } shaResult
)
{
version = version with { CommitSha = shaResult.StandardOutput?.Trim() };
}
return version;
}
async Task CloneGitRepository(string rootDir, string repositoryUrl, GitVersion? version = null)
{
// Latest if no version is given
if (version is null)
{
await RunGit(["clone", "--depth", "1", repositoryUrl], rootDir).ConfigureAwait(false);
}
else if (version.Tag is not null)
{
await RunGit(["clone", "--depth", "1", version.Tag, repositoryUrl], rootDir)
.ConfigureAwait(false);
}
else if (version.Branch is not null && version.CommitSha is not null)
{
await RunGit(["clone", "--depth", "1", "--branch", version.Branch, repositoryUrl], rootDir)
.ConfigureAwait(false);
await RunGit(["checkout", version.CommitSha, "--force"], rootDir).ConfigureAwait(false);
}
else
{
throw new ArgumentException("Version must have a tag or branch and commit sha.", nameof(version));
}
}
async Task UpdateGitRepository(string repositoryDir, string repositoryUrl, GitVersion? version = null)
{
if (version?.Tag is not null)
{
await RunGit(["init"], repositoryDir).ConfigureAwait(false);
await RunGit(["remote", "add", "origin", repositoryUrl], repositoryDir).ConfigureAwait(false);
await RunGit(["fetch", "--tags"], repositoryDir).ConfigureAwait(false);
await RunGit(["checkout", version.Tag, "--force"], repositoryDir).ConfigureAwait(false);
}
else if (version?.Branch is not null && version.CommitSha is not null)
{
await RunGit(["init"], repositoryDir).ConfigureAwait(false);
await RunGit(["remote", "add", "origin", repositoryUrl], repositoryDir).ConfigureAwait(false);
await RunGit(["fetch", "--tags"], repositoryDir).ConfigureAwait(false);
await RunGit(["checkout", version.CommitSha, "--force"], repositoryDir).ConfigureAwait(false);
}
else
{
throw new ArgumentException("Version must have a tag or branch and commit sha.", nameof(version));
}
}
Task InstallTkinterIfNecessary(IProgress<ProgressReport>? progress = null); Task InstallTkinterIfNecessary(IProgress<ProgressReport>? progress = null);
} }

33
StabilityMatrix.Core/Helper/PrerequisiteHelper.cs

@ -90,14 +90,16 @@ public class PrerequisiteHelper : IPrerequisiteHelper
result.EnsureSuccessExitCode(); result.EnsureSuccessExitCode();
} }
/// <inheritdoc />
public Task<ProcessResult> GetGitOutput(ProcessArgs args, string? workingDirectory = null)
{
throw new NotImplementedException();
}
public async Task<string> GetGitOutput(string? workingDirectory = null, params string[] args) public async Task<string> GetGitOutput(string? workingDirectory = null, params string[] args)
{ {
var output = await ProcessRunner var output = await ProcessRunner
.GetProcessOutputAsync( .GetProcessOutputAsync(GitExePath, string.Join(" ", args), workingDirectory: workingDirectory)
GitExePath,
string.Join(" ", args),
workingDirectory: workingDirectory
)
.ConfigureAwait(false); .ConfigureAwait(false);
return output; return output;
} }
@ -195,11 +197,7 @@ public class PrerequisiteHelper : IPrerequisiteHelper
public async Task UnpackResourcesIfNecessary(IProgress<ProgressReport>? progress = null) public async Task UnpackResourcesIfNecessary(IProgress<ProgressReport>? progress = null)
{ {
// Skip if all files exist // Skip if all files exist
if ( if (File.Exists(SevenZipPath) && File.Exists(PythonDllPath) && File.Exists(PythonLibraryZipPath))
File.Exists(SevenZipPath)
&& File.Exists(PythonDllPath)
&& File.Exists(PythonLibraryZipPath)
)
{ {
return; return;
} }
@ -273,10 +271,7 @@ public class PrerequisiteHelper : IPrerequisiteHelper
if (!File.Exists(SevenZipPath)) if (!File.Exists(SevenZipPath))
{ {
await ExtractEmbeddedResource("StabilityMatrix.Assets.7za.exe", AssetsDir); await ExtractEmbeddedResource("StabilityMatrix.Assets.7za.exe", AssetsDir);
await ExtractEmbeddedResource( await ExtractEmbeddedResource("StabilityMatrix.Assets.7za - LICENSE.txt", AssetsDir);
"StabilityMatrix.Assets.7za - LICENSE.txt",
AssetsDir
);
} }
// Delete existing python dir // Delete existing python dir
@ -356,10 +351,7 @@ public class PrerequisiteHelper : IPrerequisiteHelper
public async Task InstallVcRedistIfNecessary(IProgress<ProgressReport>? progress = null) public async Task InstallVcRedistIfNecessary(IProgress<ProgressReport>? progress = null)
{ {
var registry = Registry.LocalMachine; var registry = Registry.LocalMachine;
var key = registry.OpenSubKey( 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) if (key != null)
{ {
var buildId = Convert.ToUInt32(key.GetValue("Bld")); var buildId = Convert.ToUInt32(key.GetValue("Bld"));
@ -393,10 +385,7 @@ public class PrerequisiteHelper : IPrerequisiteHelper
message: "Installing prerequisites..." message: "Installing prerequisites..."
) )
); );
var process = ProcessRunner.StartAnsiProcess( var process = ProcessRunner.StartAnsiProcess(VcRedistDownloadPath, "/install /quiet /norestart");
VcRedistDownloadPath,
"/install /quiet /norestart"
);
await process.WaitForExitAsync(); await process.WaitForExitAsync();
progress?.Report( progress?.Report(
new ProgressReport( new ProgressReport(

13
StabilityMatrix.Core/Models/GitVersion.cs

@ -0,0 +1,13 @@
namespace StabilityMatrix.Core.Models;
/// <summary>
/// Union of either Tag or Branch + CommitSha.
/// </summary>
public record GitVersion
{
public string? Tag { get; init; }
public string? Branch { get; init; }
public string? CommitSha { get; init; }
}

19
StabilityMatrix.Core/Models/PackageModification/InstallExtensionStep.cs

@ -1,15 +1,24 @@
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.Packages.Extensions;
using StabilityMatrix.Core.Models.Packages.Extensions;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
namespace StabilityMatrix.Core.Models.PackageModification; namespace StabilityMatrix.Core.Models.PackageModification;
public class InstallExtensionStep(ExtensionBase extension, DirectoryPath extensionsDir) : IPackageStep public class InstallExtensionStep(
IPackageExtensionManager extensionManager,
InstalledPackage installedPackage,
PackageExtension packageExtension,
PackageExtensionVersion? extensionVersion = null
) : IPackageStep
{ {
public Task ExecuteAsync(IProgress<ProgressReport>? progress = null) public Task ExecuteAsync(IProgress<ProgressReport>? progress = null)
{ {
return extension.InstallExtensionAsync(extensionsDir, extension.MainBranch); return extensionManager.InstallExtensionAsync(
packageExtension,
installedPackage,
extensionVersion,
progress
);
} }
public string ProgressTitle => $"Installing {extension.DisplayName}"; public string ProgressTitle => $"Installing Extension {packageExtension.Title}";
} }

14
StabilityMatrix.Core/Models/Packages/BasePackage.cs

@ -1,4 +1,5 @@
using Octokit; using System.Diagnostics.CodeAnalysis;
using Octokit;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.HardwareInfo; using StabilityMatrix.Core.Helper.HardwareInfo;
using StabilityMatrix.Core.Models.Database; using StabilityMatrix.Core.Models.Database;
@ -158,6 +159,17 @@ public abstract class BasePackage
public abstract Dictionary<SharedFolderType, IReadOnlyList<string>>? SharedFolders { get; } public abstract Dictionary<SharedFolderType, IReadOnlyList<string>>? SharedFolders { get; }
public abstract Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders { get; } public abstract Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders { get; }
/// <summary>
/// If defined, this package supports extensions using this manager.
/// </summary>
public virtual IPackageExtensionManager? ExtensionManager { get; }
/// <summary>
/// True if this package supports extensions.
/// </summary>
[MemberNotNullWhen(true)]
public virtual bool SupportsExtensions => ExtensionManager is not null;
public abstract Task<PackageVersionOptions> GetAllVersionOptions(); public abstract Task<PackageVersionOptions> GetAllVersionOptions();
public abstract Task<IEnumerable<GitCommit>?> GetAllCommits( public abstract Task<IEnumerable<GitCommit>?> GetAllCommits(
string branch, string branch,

36
StabilityMatrix.Core/Models/Packages/ComfyUI.cs

@ -1,4 +1,6 @@
using System.Diagnostics; using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using NLog; using NLog;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
@ -462,4 +464,38 @@ public class ComfyUI(
await extraPathsYamlPath.WriteAllTextAsync(yamlData).ConfigureAwait(false); await extraPathsYamlPath.WriteAllTextAsync(yamlData).ConfigureAwait(false);
} }
public override IPackageExtensionManager ExtensionManager => new ComfyExtensionManager(this);
private class ComfyExtensionManager(ComfyUI package)
: GitPackageExtensionManager(package.PrerequisiteHelper)
{
public override string RelativeInstallDirectory => "custom_nodes";
protected override IEnumerable<ExtensionManifest> GetManifests(InstalledPackage installedPackage) =>
[
new ExtensionManifest(
new Uri("https://github.com/ltdrdata/ComfyUI-Manager/blob/main/custom-node-list.json")
)
];
public override async Task<IEnumerable<PackageExtension>> GetManifestExtensionsAsync(
ExtensionManifest manifest,
CancellationToken cancellationToken = default
)
{
// Get json
var content = await package
.DownloadService.GetContentAsync(manifest.Uri.ToString(), cancellationToken)
.ConfigureAwait(false);
// Parse json
var jsonManifest = JsonSerializer.Deserialize<ComfyExtensionManifest>(
content,
ComfyExtensionManifestSerializerContext.Default.Options
);
return jsonManifest?.GetPackageExtensions() ?? Enumerable.Empty<PackageExtension>();
}
}
} }

43
StabilityMatrix.Core/Models/Packages/Extensions/ComfyExtensionManifest.cs

@ -0,0 +1,43 @@
using System.Text.Json.Serialization;
namespace StabilityMatrix.Core.Models.Packages.Extensions;
public record ComfyExtensionManifest
{
public required IEnumerable<ManifestEntry> CustomNodes { get; init; }
public IEnumerable<PackageExtension> GetPackageExtensions()
{
return CustomNodes.Select(
x =>
new PackageExtension
{
Author = x.Author,
Title = x.Title,
Reference = x.Reference,
Files = x.Files,
Description = x.Description,
InstallType = x.InstallType
}
);
}
public record ManifestEntry
{
public required string Author { get; init; }
public required string Title { get; init; }
public required Uri Reference { get; init; }
public required IEnumerable<Uri> Files { get; init; }
public string? Description { get; init; }
public string? InstallType { get; init; }
}
}
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower)]
[JsonSerializable(typeof(ComfyExtensionManifest))]
internal partial class ComfyExtensionManifestSerializerContext : JsonSerializerContext;

24
StabilityMatrix.Core/Models/Packages/Extensions/ExtensionBase.cs

@ -1,24 +0,0 @@
using StabilityMatrix.Core.Models.FileInterfaces;
namespace StabilityMatrix.Core.Models.Packages.Extensions;
public abstract class ExtensionBase
{
public string ByAuthor => $"By {Author}";
public abstract string RepoName { get; }
public abstract string DisplayName { get; set; }
public abstract string Author { get; }
public abstract string Blurb { get; }
public abstract IEnumerable<string> CompatibleWith { get; }
public abstract string MainBranch { get; }
public abstract Task InstallExtensionAsync(
DirectoryPath installDirectory,
string branch,
CancellationToken cancellationToken = default
);
public string GithubUrl => $"https://github.com/{Author}/{RepoName}";
}

3
StabilityMatrix.Core/Models/Packages/Extensions/ExtensionManifest.cs

@ -0,0 +1,3 @@
namespace StabilityMatrix.Core.Models.Packages.Extensions;
public record ExtensionManifest(Uri Uri);

144
StabilityMatrix.Core/Models/Packages/Extensions/GitPackageExtensionManager.cs

@ -0,0 +1,144 @@
using KGySoft.CoreLibraries;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress;
namespace StabilityMatrix.Core.Models.Packages.Extensions;
public abstract class GitPackageExtensionManager(IPrerequisiteHelper prerequisiteHelper)
: IPackageExtensionManager
{
public abstract string RelativeInstallDirectory { get; }
protected virtual IEnumerable<string> IndexRelativeDirectories => [RelativeInstallDirectory];
public abstract Task<IEnumerable<PackageExtension>> GetManifestExtensionsAsync(
ExtensionManifest manifest,
CancellationToken cancellationToken = default
);
/// <inheritdoc />
Task<IEnumerable<PackageExtension>> IPackageExtensionManager.GetManifestExtensionsAsync(
ExtensionManifest manifest,
CancellationToken cancellationToken
)
{
return GetManifestExtensionsAsync(manifest, cancellationToken);
}
protected abstract IEnumerable<ExtensionManifest> GetManifests(InstalledPackage installedPackage);
/// <inheritdoc />
IEnumerable<ExtensionManifest> IPackageExtensionManager.GetManifests(InstalledPackage installedPackage)
{
return GetManifests(installedPackage);
}
/// <inheritdoc />
public async Task<IEnumerable<InstalledPackageExtension>> GetInstalledExtensionsAsync(
InstalledPackage installedPackage,
CancellationToken cancellationToken = default
)
{
if (installedPackage.FullPath is not { } packagePath)
{
return Enumerable.Empty<InstalledPackageExtension>();
}
var extensions = new List<InstalledPackageExtension>();
// Search for installed extensions in the package's index directories.
foreach (
var indexDirectory in IndexRelativeDirectories.Select(
path => new DirectoryPath(packagePath, path)
)
)
{
cancellationToken.ThrowIfCancellationRequested();
// Check subdirectories of the index directory
foreach (var subDirectory in indexDirectory.EnumerateDirectories())
{
cancellationToken.ThrowIfCancellationRequested();
// Skip if not valid git repository
if (await prerequisiteHelper.CheckIsGitRepository(subDirectory).ConfigureAwait(false) != true)
continue;
// Get git version
var version = await prerequisiteHelper
.GetGitRepositoryVersion(subDirectory)
.ConfigureAwait(false);
extensions.Add(
new InstalledPackageExtension
{
Paths = [subDirectory],
Version = new PackageExtensionVersion
{
Tag = version.Tag,
Branch = version.Branch,
CommitSha = version.CommitSha
}
}
);
}
}
return extensions;
}
/// <inheritdoc />
public async Task InstallExtensionAsync(
PackageExtension extension,
InstalledPackage installedPackage,
PackageExtensionVersion? version = null,
IProgress<ProgressReport>? progress = null,
CancellationToken cancellationToken = default
)
{
if (installedPackage.FullPath is not { } packagePath)
throw new ArgumentException("Package must have a valid path.", nameof(installedPackage));
// Ensure type
if (extension.InstallType?.ToLowerInvariant() != "git-clone")
{
throw new ArgumentException(
$"Extension must have install type 'git-clone' but has '{extension.InstallType}'.",
nameof(extension)
);
}
// Git clone all files
var cloneRoot = new DirectoryPath(packagePath, RelativeInstallDirectory);
foreach (var repositoryUri in extension.Files)
{
cancellationToken.ThrowIfCancellationRequested();
progress?.Report(new ProgressReport(0f, $"Cloning {repositoryUri}", isIndeterminate: true));
await prerequisiteHelper
.CloneGitRepository(cloneRoot, repositoryUri.ToString(), version)
.ConfigureAwait(false);
progress?.Report(new ProgressReport(1f, $"Cloned {repositoryUri}"));
}
}
/// <inheritdoc />
public async Task UninstallExtensionAsync(
InstalledPackageExtension installedExtension,
InstalledPackage installedPackage,
IProgress<ProgressReport>? progress = null,
CancellationToken cancellationToken = default
)
{
foreach (var path in installedExtension.Paths.Where(p => p.Exists))
{
cancellationToken.ThrowIfCancellationRequested();
await path.DeleteAsync().ConfigureAwait(false);
}
}
}

37
StabilityMatrix.Core/Models/Packages/Extensions/IPackageExtensionManager.cs

@ -0,0 +1,37 @@
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress;
namespace StabilityMatrix.Core.Models.Packages.Extensions;
/// <summary>
/// Interface for a package extension manager.
/// </summary>
public interface IPackageExtensionManager
{
IEnumerable<ExtensionManifest> GetManifests(InstalledPackage installedPackage);
Task<IEnumerable<PackageExtension>> GetManifestExtensionsAsync(
ExtensionManifest manifest,
CancellationToken cancellationToken = default
);
Task<IEnumerable<InstalledPackageExtension>> GetInstalledExtensionsAsync(
InstalledPackage installedPackage,
CancellationToken cancellationToken = default
);
Task InstallExtensionAsync(
PackageExtension extension,
InstalledPackage installedPackage,
PackageExtensionVersion? version = null,
IProgress<ProgressReport>? progress = null,
CancellationToken cancellationToken = default
);
Task UninstallExtensionAsync(
InstalledPackageExtension installedExtension,
InstalledPackage installedPackage,
IProgress<ProgressReport>? progress = null,
CancellationToken cancellationToken = default
);
}

21
StabilityMatrix.Core/Models/Packages/Extensions/InstalledPackageExtension.cs

@ -0,0 +1,21 @@
using StabilityMatrix.Core.Models.FileInterfaces;
namespace StabilityMatrix.Core.Models.Packages.Extensions;
public record InstalledPackageExtension
{
/// <summary>
/// All folders or files of the extension.
/// </summary>
public required IEnumerable<IPathObject> Paths { get; init; }
/// <summary>
/// The version of the extension.
/// </summary>
public PackageExtensionVersion? Version { get; init; }
/// <summary>
///
/// </summary>
public PackageExtension? Definition { get; init; }
}

16
StabilityMatrix.Core/Models/Packages/Extensions/PackageExtension.cs

@ -0,0 +1,16 @@
namespace StabilityMatrix.Core.Models.Packages.Extensions;
public record PackageExtension
{
public required string Author { get; init; }
public required string Title { get; init; }
public required Uri Reference { get; init; }
public required IEnumerable<Uri> Files { get; init; }
public string? Description { get; init; }
public string? InstallType { get; init; }
}

3
StabilityMatrix.Core/Models/Packages/Extensions/PackageExtensionVersion.cs

@ -0,0 +1,3 @@
namespace StabilityMatrix.Core.Models.Packages.Extensions;
public record PackageExtensionVersion : GitVersion;
Loading…
Cancel
Save