Browse Source

Merge pull request #465 from ionite34/fix-invoke

install node/pnpm and build invoke frontend during install
pull/438/head
JT 10 months ago committed by GitHub
parent
commit
1a4aa0dc15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      CHANGELOG.md
  2. 82
      StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs
  3. 57
      StabilityMatrix.Avalonia/Helpers/WindowsPrerequisiteHelper.cs
  4. 14
      StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageInstallDetailViewModel.cs
  5. 1
      StabilityMatrix.Avalonia/Views/PackageManager/PackageInstallDetailView.axaml
  6. 6
      StabilityMatrix.Core/Helper/IPrerequisiteHelper.cs
  7. 14
      StabilityMatrix.Core/Helper/PrerequisiteHelper.cs
  8. 139
      StabilityMatrix.Core/Models/Packages/InvokeAI.cs
  9. 4
      StabilityMatrix.Core/Processes/ProcessArgs.cs

2
CHANGELOG.md

@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2
## v2.8.0-pre.2
### Fixed
- Fixed Auto-update failing to start new version on Windows and Linux when path contains spaces
- Fixed InvokeAI v3.6.0 `"detail": "Not Found"` error when opening the UI
- Install button will now be properly disabled when the duplicate warning is shown
## v2.8.0-pre.1
### Added

82
StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs

@ -4,6 +4,7 @@ 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;
@ -32,10 +33,13 @@ public class UnixPrerequisiteHelper : IPrerequisiteHelper
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);
// Cached store of whether or not git is installed
private bool? isGitInstalled;
@ -231,6 +235,82 @@ public class UnixPrerequisiteHelper : IPrerequisiteHelper
return ProcessRunner.RunBashCommand(args.Prepend("git").ToArray(), workingDirectory ?? "");
}
[SupportedOSPlatform("Linux")]
[SupportedOSPlatform("macOS")]
public async Task RunNpm(
ProcessArgs args,
string? workingDirectory = null,
Action<ProcessOutput>? onProcessOutput = null
)
{
var command = args.Prepend([NpmPath]);
var result = await ProcessRunner.RunBashCommand(command.ToArray(), workingDirectory ?? "");
if (result.ExitCode != 0)
{
Logger.Error(
"npm command [{Command}] failed with exit code " + "{ExitCode}:\n{StdOut}\n{StdErr}",
command,
result.ExitCode,
result.StandardOutput,
result.StandardError
);
throw new ProcessException(
$"npm command [{command}] failed with exit code"
+ $" {result.ExitCode}:\n{result.StandardOutput}\n{result.StandardError}"
);
}
onProcessOutput?.Invoke(ProcessOutput.FromStdOutLine(result.StandardOutput));
onProcessOutput?.Invoke(ProcessOutput.FromStdErrLine(result.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.xz";
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);
}
[UnsupportedOSPlatform("Linux")]
[UnsupportedOSPlatform("macOS")]
public Task InstallTkinterIfNecessary(IProgress<ProgressReport>? progress = null)

57
StabilityMatrix.Avalonia/Helpers/WindowsPrerequisiteHelper.cs

@ -26,6 +26,7 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
private const string VcRedistDownloadUrl = "https://aka.ms/vs/16/release/vc_redist.x64.exe";
private const string TkinterDownloadUrl =
"https://cdn.lykos.ai/tkinter-cpython-embedded-3.10.11-win-x64.zip";
private const string NodeDownloadUrl = "https://nodejs.org/dist/v20.11.0/node-v20.11.0-win-x64.zip";
private string HomeDir => settingsManager.LibraryDir;
@ -49,8 +50,10 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
private string TkinterZipPath => Path.Combine(AssetsDir, "tkinter.zip");
private string TkinterExtractPath => PythonDir;
private string TkinterExistsPath => Path.Combine(PythonDir, "tkinter");
public string GitBinPath => Path.Combine(PortableGitInstallDir, "bin");
private string NodeExistsPath => Path.Combine(AssetsDir, "nodejs", "npm.cmd");
private string NodeDownloadPath => Path.Combine(AssetsDir, "nodejs.zip");
public string GitBinPath => Path.Combine(PortableGitInstallDir, "bin");
public bool IsPythonInstalled => File.Exists(PythonDllPath);
public WindowsPrerequisiteHelper(
@ -109,12 +112,28 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
);
}
public async Task RunNpm(
ProcessArgs args,
string? workingDirectory = null,
Action<ProcessOutput>? onProcessOutput = null
)
{
var result = await ProcessRunner
.GetProcessResultAsync(NodeExistsPath, args, workingDirectory)
.ConfigureAwait(false);
result.EnsureSuccessExitCode();
onProcessOutput?.Invoke(ProcessOutput.FromStdOutLine(result.StandardOutput));
onProcessOutput?.Invoke(ProcessOutput.FromStdErrLine(result.StandardError));
}
public async Task InstallAllIfNecessary(IProgress<ProgressReport>? progress = null)
{
await InstallVcRedistIfNecessary(progress);
await UnpackResourcesIfNecessary(progress);
await InstallPythonIfNecessary(progress);
await InstallGitIfNecessary(progress);
await InstallNodeIfNecessary(progress);
}
public async Task UnpackResourcesIfNecessary(IProgress<ProgressReport>? progress = null)
@ -348,6 +367,42 @@ public class WindowsPrerequisiteHelper : IPrerequisiteHelper
File.Delete(VcRedistDownloadPath);
}
[SupportedOSPlatform("windows")]
public async Task InstallNodeIfNecessary(IProgress<ProgressReport>? progress = null)
{
if (File.Exists(NodeExistsPath))
{
Logger.Info("node already installed");
return;
}
Logger.Info("Downloading node");
await downloadService.DownloadToFileAsync(NodeDownloadUrl, 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.Extract(NodeDownloadPath, AssetsDir, progress);
// move to assets dir
var existingNodeDir = Path.Combine(AssetsDir, "node-v20.11.0-win-x64");
Directory.Move(existingNodeDir, Path.Combine(AssetsDir, "nodejs"));
progress?.Report(
new ProgressReport(progress: 1f, message: "Node install complete", type: ProgressType.Generic)
);
File.Delete(NodeDownloadPath);
}
private async Task UnzipGit(IProgress<ProgressReport>? progress = null)
{
if (progress == null)

14
StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageInstallDetailViewModel.cs

@ -82,6 +82,9 @@ public partial class PackageInstallDetailViewModel(
[ObservableProperty]
private GitCommit? selectedCommit;
[ObservableProperty]
private bool canInstall;
private PackageVersionOptions? allOptions;
public override async Task OnLoadedAsync()
@ -104,6 +107,8 @@ public partial class PackageInstallDetailViewModel(
UpdateVersions();
await UpdateCommits(SelectedPackage.MainBranch);
}
CanInstall = !ShowDuplicateWarning;
}
[RelayCommand]
@ -214,6 +219,8 @@ public partial class PackageInstallDetailViewModel(
private void UpdateVersions()
{
CanInstall = false;
AvailableVersions =
IsReleaseMode && ShowReleaseMode ? allOptions.AvailableVersions : allOptions.AvailableBranches;
@ -221,16 +228,22 @@ public partial class PackageInstallDetailViewModel(
? AvailableVersions?.FirstOrDefault(x => x.TagName == SelectedPackage.MainBranch)
?? AvailableVersions?.FirstOrDefault()
: AvailableVersions?.FirstOrDefault();
CanInstall = !ShowDuplicateWarning;
}
private async Task UpdateCommits(string branchName)
{
CanInstall = false;
var commits = await SelectedPackage.GetAllCommits(branchName);
if (commits != null)
{
AvailableCommits = new ObservableCollection<GitCommit>(commits);
SelectedCommit = AvailableCommits.FirstOrDefault();
}
CanInstall = !ShowDuplicateWarning;
}
partial void OnInstallNameChanged(string? value)
@ -238,6 +251,7 @@ public partial class PackageInstallDetailViewModel(
ShowDuplicateWarning = settingsManager.Settings.InstalledPackages.Any(
p => p.LibraryPath == $"Packages{Path.DirectorySeparatorChar}{value}"
);
CanInstall = !ShowDuplicateWarning;
}
partial void OnIsReleaseModeChanged(bool value)

1
StabilityMatrix.Avalonia/Views/PackageManager/PackageInstallDetailView.axaml

@ -169,6 +169,7 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
FontSize="16"
IsEnabled="{Binding CanInstall}"
Command="{Binding InstallCommand}"
Content="{x:Static lang:Resources.Action_Install}" />

6
StabilityMatrix.Core/Helper/IPrerequisiteHelper.cs

@ -154,4 +154,10 @@ public interface IPrerequisiteHelper
}
Task InstallTkinterIfNecessary(IProgress<ProgressReport>? progress = null);
Task RunNpm(
ProcessArgs args,
string? workingDirectory = null,
Action<ProcessOutput>? onProcessOutput = null
);
Task InstallNodeIfNecessary(IProgress<ProgressReport>? progress = null);
}

14
StabilityMatrix.Core/Helper/PrerequisiteHelper.cs

@ -109,6 +109,20 @@ public class PrerequisiteHelper : IPrerequisiteHelper
throw new NotImplementedException();
}
public Task RunNpm(
ProcessArgs args,
string? workingDirectory = null,
Action<ProcessOutput>? onProcessOutput = null
)
{
throw new NotImplementedException();
}
public Task InstallNodeIfNecessary(IProgress<ProgressReport>? progress = null)
{
throw new NotImplementedException();
}
public async Task InstallAllIfNecessary(IProgress<ProgressReport>? progress = null)
{
await InstallVcRedistIfNecessary(progress);

139
StabilityMatrix.Core/Models/Packages/InvokeAI.cs

@ -1,4 +1,5 @@
using System.Globalization;
using System.Diagnostics;
using System.Globalization;
using System.Text.RegularExpressions;
using NLog;
using StabilityMatrix.Core.Attributes;
@ -18,6 +19,7 @@ public class InvokeAI : BaseGitPackage
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
private const string RelativeRootPath = "invokeai-root";
private string RelativeFrontendBuildPath = Path.Combine("invokeai", "frontend", "web", "dist");
public override string Name => "InvokeAI";
public override string DisplayName { get; set; } = "InvokeAI";
@ -61,10 +63,19 @@ public class InvokeAI : BaseGitPackage
public override Dictionary<SharedFolderType, IReadOnlyList<string>> SharedFolders =>
new()
{
[SharedFolderType.StableDiffusion] = new[] { Path.Combine(RelativeRootPath, "autoimport", "main") },
[SharedFolderType.StableDiffusion] = new[]
{
Path.Combine(RelativeRootPath, "autoimport", "main")
},
[SharedFolderType.Lora] = new[] { Path.Combine(RelativeRootPath, "autoimport", "lora") },
[SharedFolderType.TextualInversion] = new[] { Path.Combine(RelativeRootPath, "autoimport", "embedding") },
[SharedFolderType.ControlNet] = new[] { Path.Combine(RelativeRootPath, "autoimport", "controlnet") },
[SharedFolderType.TextualInversion] = new[]
{
Path.Combine(RelativeRootPath, "autoimport", "embedding")
},
[SharedFolderType.ControlNet] = new[]
{
Path.Combine(RelativeRootPath, "autoimport", "controlnet")
},
[SharedFolderType.InvokeIpAdapters15] = new[]
{
Path.Combine(RelativeRootPath, "models", "sd-1", "ip_adapter")
@ -77,7 +88,10 @@ public class InvokeAI : BaseGitPackage
{
Path.Combine(RelativeRootPath, "models", "any", "clip_vision")
},
[SharedFolderType.T2IAdapter] = new[] { Path.Combine(RelativeRootPath, "autoimport", "t2i_adapter") }
[SharedFolderType.T2IAdapter] = new[]
{
Path.Combine(RelativeRootPath, "autoimport", "t2i_adapter")
}
};
public override Dictionary<SharedOutputType, IReadOnlyList<string>>? SharedOutputFolders =>
@ -168,13 +182,23 @@ public class InvokeAI : BaseGitPackage
venvRunner.EnvironmentVariables = GetEnvVars(installLocation);
progress?.Report(new ProgressReport(-1f, "Installing Package", isIndeterminate: true));
await SetupAndBuildInvokeFrontend(
installLocation,
progress,
onConsoleOutput,
venvRunner.EnvironmentVariables
)
.ConfigureAwait(false);
var pipCommandArgs = "-e . --use-pep517 --extra-index-url https://download.pytorch.org/whl/cpu";
switch (torchVersion)
{
// If has Nvidia Gpu, install CUDA version
case TorchVersion.Cuda:
progress?.Report(new ProgressReport(-1f, "Installing PyTorch for CUDA", isIndeterminate: true));
progress?.Report(
new ProgressReport(-1f, "Installing PyTorch for CUDA", isIndeterminate: true)
);
var args = new List<Argument>();
if (exists)
@ -200,18 +224,23 @@ public class InvokeAI : BaseGitPackage
.ConfigureAwait(false);
Logger.Info("Starting InvokeAI install (CUDA)...");
pipCommandArgs = "-e .[xformers] --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu121";
pipCommandArgs =
"-e .[xformers] --use-pep517 --extra-index-url https://download.pytorch.org/whl/cu121";
break;
// For AMD, Install ROCm version
case TorchVersion.Rocm:
await venvRunner
.PipInstall(
new PipInstallArgs().WithTorch("==2.0.1").WithTorchVision().WithExtraIndex("rocm5.4.2"),
new PipInstallArgs()
.WithTorch("==2.0.1")
.WithTorchVision()
.WithExtraIndex("rocm5.4.2"),
onConsoleOutput
)
.ConfigureAwait(false);
Logger.Info("Starting InvokeAI install (ROCm)...");
pipCommandArgs = "-e . --use-pep517 --extra-index-url https://download.pytorch.org/whl/rocm5.4.2";
pipCommandArgs =
"-e . --use-pep517 --extra-index-url https://download.pytorch.org/whl/rocm5.4.2";
break;
case TorchVersion.Mps:
// For Apple silicon, use MPS
@ -246,6 +275,58 @@ public class InvokeAI : BaseGitPackage
progress?.Report(new ProgressReport(1f, "Done!", isIndeterminate: false));
}
private async Task SetupAndBuildInvokeFrontend(
string installLocation,
IProgress<ProgressReport>? progress,
Action<ProcessOutput>? onConsoleOutput,
IReadOnlyDictionary<string, string>? envVars = null
)
{
await PrerequisiteHelper.InstallNodeIfNecessary(progress).ConfigureAwait(false);
await PrerequisiteHelper.RunNpm(["i", "pnpm"], installLocation).ConfigureAwait(false);
if (Compat.IsMacOS || Compat.IsLinux)
{
await PrerequisiteHelper.RunNpm(["i", "vite"], installLocation).ConfigureAwait(false);
}
var pnpmPath = Path.Combine(
installLocation,
"node_modules",
".bin",
Compat.IsWindows ? "pnpm.cmd" : "pnpm"
);
var vitePath = Path.Combine(
installLocation,
"node_modules",
".bin",
Compat.IsWindows ? "vite.cmd" : "vite"
);
var invokeFrontendPath = Path.Combine(installLocation, "invokeai", "frontend", "web");
var process = ProcessRunner.StartAnsiProcess(
pnpmPath,
"i --ignore-scripts=true",
invokeFrontendPath,
onConsoleOutput,
envVars
);
await process.WaitForExitAsync().ConfigureAwait(false);
process = ProcessRunner.StartAnsiProcess(
Compat.IsWindows ? pnpmPath : vitePath,
"build",
invokeFrontendPath,
onConsoleOutput,
envVars
);
await process.WaitForExitAsync().ConfigureAwait(false);
}
public override Task RunPackage(
string installedPackagePath,
string command,
@ -277,6 +358,19 @@ public class InvokeAI : BaseGitPackage
VenvRunner.EnvironmentVariables = GetEnvVars(installedPackagePath);
// fix frontend build missing for people who updated to v3.6 before the fix
var frontendExistsPath = Path.Combine(installedPackagePath, RelativeFrontendBuildPath);
if (!Directory.Exists(frontendExistsPath))
{
await SetupAndBuildInvokeFrontend(
installedPackagePath,
null,
onConsoleOutput,
VenvRunner.EnvironmentVariables
)
.ConfigureAwait(false);
}
// Launch command is for a console entry point, and not a direct script
var entryPoint = await VenvRunner.GetEntryPoint(command).ConfigureAwait(false);
@ -320,7 +414,9 @@ public class InvokeAI : BaseGitPackage
{
onConsoleOutput?.Invoke(s);
if (spam3 && s.Text.Contains("[3] Accept the best guess;", StringComparison.OrdinalIgnoreCase))
if (
spam3 && s.Text.Contains("[3] Accept the best guess;", StringComparison.OrdinalIgnoreCase)
)
{
VenvRunner.Process?.StandardInput.WriteLine("3");
return;
@ -362,6 +458,29 @@ public class InvokeAI : BaseGitPackage
root.Create();
env["INVOKEAI_ROOT"] = root;
if (env.ContainsKey("PATH"))
{
env["PATH"] +=
$"{Compat.PathDelimiter}{Path.Combine(SettingsManager.LibraryDir, "Assets", "nodejs")}";
}
else
{
env["PATH"] = Path.Combine(SettingsManager.LibraryDir, "Assets", "nodejs");
}
env["PATH"] += $"{Compat.PathDelimiter}{Path.Combine(installPath, "node_modules", ".bin")}";
if (Compat.IsMacOS || Compat.IsLinux)
{
env["PATH"] +=
$"{Compat.PathDelimiter}{Path.Combine(SettingsManager.LibraryDir, "Assets", "nodejs", "bin")}";
}
if (Compat.IsWindows)
{
env["PATH"] +=
$"{Compat.PathDelimiter}{Environment.GetFolderPath(Environment.SpecialFolder.System)}";
}
return env;
}
}

4
StabilityMatrix.Core/Processes/ProcessArgs.cs

@ -70,7 +70,7 @@ public partial class ProcessArgs : OneOfBase<string, string[]>, IEnumerable<stri
private static partial Regex ArgumentsRegex();
}
internal static class ProcessArgsCollectionBuilder
public static class ProcessArgsCollectionBuilder
{
internal static ProcessArgs Create(ReadOnlySpan<string> values) => new(values.ToArray());
public static ProcessArgs Create(ReadOnlySpan<string> values) => new(values.ToArray());
}

Loading…
Cancel
Save