Browse Source

PyVenv fixes and reformat

pull/55/head
Ionite 1 year ago
parent
commit
0a423640fe
No known key found for this signature in database
  1. 197
      StabilityMatrix.Core/Python/PyVenvRunner.cs

197
StabilityMatrix.Core/Python/PyVenvRunner.cs

@ -20,20 +20,20 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
public const string TorchPipInstallArgsCuda = public const string TorchPipInstallArgsCuda =
"torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu118"; "torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu118";
public const string TorchPipInstallArgsCpu = public const string TorchPipInstallArgsCpu = "torch torchvision torchaudio";
"torch torchvision torchaudio"; public const string TorchPipInstallArgsDirectML = "torch-directml";
public const string TorchPipInstallArgsDirectML =
"torch-directml";
/// <summary> /// <summary>
/// Relative path to the site-packages folder from the venv root. /// Relative path to the site-packages folder from the venv root.
/// This is platform specific. /// This is platform specific.
/// </summary> /// </summary>
public static string RelativeSitePackagesPath => Compat.Switch( public static string RelativeSitePackagesPath =>
(PlatformKind.Windows, "Lib/site-packages"), Compat.Switch(
(PlatformKind.Unix, "lib/python3.10/site-packages")); (PlatformKind.Windows, "Lib/site-packages"),
(PlatformKind.Unix, "lib/python3.10/site-packages")
);
/// <summary> /// <summary>
/// The process running the python executable. /// The process running the python executable.
/// </summary> /// </summary>
@ -48,51 +48,54 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
/// Optional working directory for the python process. /// Optional working directory for the python process.
/// </summary> /// </summary>
public DirectoryPath? WorkingDirectory { get; set; } public DirectoryPath? WorkingDirectory { get; set; }
/// <summary> /// <summary>
/// Optional environment variables for the python process. /// Optional environment variables for the python process.
/// </summary> /// </summary>
public IReadOnlyDictionary<string, string>? EnvironmentVariables { get; set; } public IReadOnlyDictionary<string, string>? EnvironmentVariables { get; set; }
/// <summary> /// <summary>
/// Name of the python binary folder. /// Name of the python binary folder.
/// 'Scripts' on Windows, 'bin' on Unix. /// 'Scripts' on Windows, 'bin' on Unix.
/// </summary> /// </summary>
public static string RelativeBinPath => Compat.Switch( public static string RelativeBinPath =>
(PlatformKind.Windows, "Scripts"), Compat.Switch((PlatformKind.Windows, "Scripts"), (PlatformKind.Unix, "bin"));
(PlatformKind.Unix, "bin"));
/// <summary> /// <summary>
/// The relative path to the python executable. /// The relative path to the python executable.
/// </summary> /// </summary>
public static string RelativePythonPath => Compat.Switch( public static string RelativePythonPath =>
(PlatformKind.Windows, Path.Combine("Scripts", "python.exe")), Compat.Switch(
(PlatformKind.Unix, Path.Combine("bin", "python3"))); (PlatformKind.Windows, Path.Combine("Scripts", "python.exe")),
(PlatformKind.Unix, Path.Combine("bin", "python3"))
);
/// <summary> /// <summary>
/// The full path to the python executable. /// The full path to the python executable.
/// </summary> /// </summary>
public FilePath PythonPath => RootPath.JoinFile(RelativePythonPath); public FilePath PythonPath => RootPath.JoinFile(RelativePythonPath);
/// <summary> /// <summary>
/// The relative path to the pip executable. /// The relative path to the pip executable.
/// </summary> /// </summary>
public static string RelativePipPath => Compat.Switch( public static string RelativePipPath =>
(PlatformKind.Windows, Path.Combine("Scripts", "pip.exe")), Compat.Switch(
(PlatformKind.Unix, Path.Combine("bin", "pip3"))); (PlatformKind.Windows, Path.Combine("Scripts", "pip.exe")),
(PlatformKind.Unix, Path.Combine("bin", "pip3"))
);
/// <summary> /// <summary>
/// The full path to the pip executable. /// The full path to the pip executable.
/// </summary> /// </summary>
public FilePath PipPath => RootPath.JoinFile(RelativePipPath); public FilePath PipPath => RootPath.JoinFile(RelativePipPath);
/// <summary> /// <summary>
/// List of substrings to suppress from the output. /// List of substrings to suppress from the output.
/// When a line contains any of these substrings, it will not be forwarded to callbacks. /// When a line contains any of these substrings, it will not be forwarded to callbacks.
/// A corresponding Info log will be written instead. /// A corresponding Info log will be written instead.
/// </summary> /// </summary>
public List<string> SuppressOutput { get; } = new() { "fatal: not a git repository" }; public List<string> SuppressOutput { get; } = new() { "fatal: not a git repository" };
public PyVenvRunner(DirectoryPath path) public PyVenvRunner(DirectoryPath path)
{ {
RootPath = path; RootPath = path;
@ -115,8 +118,13 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
RootPath.Create(); RootPath.Create();
// Create venv (copy mode if windows) // Create venv (copy mode if windows)
var args = new string[] { "-m", "virtualenv", var args = new string[]
Compat.IsWindows ? "--always-copy" : "", RootPath }; {
"-m",
"virtualenv",
Compat.IsWindows ? "--always-copy" : "",
RootPath
};
var venvProc = ProcessRunner.StartAnsiProcess(PyRunner.PythonExePath, args); var venvProc = ProcessRunner.StartAnsiProcess(PyRunner.PythonExePath, args);
await venvProc.WaitForExitAsync().ConfigureAwait(false); await venvProc.WaitForExitAsync().ConfigureAwait(false);
@ -126,7 +134,9 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
{ {
var output = await venvProc.StandardOutput.ReadToEndAsync().ConfigureAwait(false); var output = await venvProc.StandardOutput.ReadToEndAsync().ConfigureAwait(false);
output += await venvProc.StandardError.ReadToEndAsync().ConfigureAwait(false); output += await venvProc.StandardError.ReadToEndAsync().ConfigureAwait(false);
throw new InvalidOperationException($"Venv creation failed with code {returnCode}: {output}"); throw new InvalidOperationException(
$"Venv creation failed with code {returnCode}: {output}"
);
} }
} }
@ -137,7 +147,8 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
private void SetPyvenvCfg(string pythonDirectory) private void SetPyvenvCfg(string pythonDirectory)
{ {
// Skip if we are not created yet // Skip if we are not created yet
if (!Exists()) return; if (!Exists())
return;
// Path to pyvenv.cfg // Path to pyvenv.cfg
var cfgPath = Path.Combine(RootPath, "pyvenv.cfg"); var cfgPath = Path.Combine(RootPath, "pyvenv.cfg");
@ -145,22 +156,25 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
{ {
throw new FileNotFoundException("pyvenv.cfg not found", cfgPath); throw new FileNotFoundException("pyvenv.cfg not found", cfgPath);
} }
Logger.Info("Updating pyvenv.cfg with embedded Python directory {PyDir}", pythonDirectory); Logger.Info("Updating pyvenv.cfg with embedded Python directory {PyDir}", pythonDirectory);
// Insert a top section // Insert a top section
var topSection = "[top]" + Environment.NewLine; var topSection = "[top]" + Environment.NewLine;
var cfg = new ConfigParser(topSection + File.ReadAllText(cfgPath)); var cfg = new ConfigParser(topSection + File.ReadAllText(cfgPath));
// Need to set all path keys - home, base-prefix, base-exec-prefix, base-executable // Need to set all path keys - home, base-prefix, base-exec-prefix, base-executable
cfg.SetValue("top", "home", pythonDirectory); cfg.SetValue("top", "home", pythonDirectory);
cfg.SetValue("top", "base-prefix", pythonDirectory); cfg.SetValue("top", "base-prefix", pythonDirectory);
cfg.SetValue("top", "base-exec-prefix", pythonDirectory); cfg.SetValue("top", "base-exec-prefix", pythonDirectory);
cfg.SetValue("top", "base-executable", cfg.SetValue(
Path.Combine(pythonDirectory, Compat.IsWindows ? "python.exe" : RelativePythonPath)); "top",
"base-executable",
Path.Combine(pythonDirectory, Compat.IsWindows ? "python.exe" : RelativePythonPath)
);
// Convert to string for writing, strip the top section // Convert to string for writing, strip the top section
var cfgString = cfg.ToString()!.Replace(topSection, ""); var cfgString = cfg.ToString()!.Replace(topSection, "");
File.WriteAllText(cfgPath, cfgString); File.WriteAllText(cfgPath, cfgString);
@ -176,31 +190,35 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
{ {
throw new FileNotFoundException("pip not found", PipPath); throw new FileNotFoundException("pip not found", PipPath);
} }
// Record output for errors // Record output for errors
var output = new StringBuilder(); var output = new StringBuilder();
var outputAction = outputDataReceived == null ? null : new Action<ProcessOutput>(s => var outputAction =
{ outputDataReceived == null
Logger.Debug($"Pip output: {s.Text}"); ? null
// Record to output : new Action<ProcessOutput>(s =>
output.Append(s.Text); {
// Forward to callback Logger.Debug($"Pip output: {s.Text}");
outputDataReceived(s); // Record to output
}); output.Append(s.Text);
// Forward to callback
outputDataReceived(s);
});
SetPyvenvCfg(PyRunner.PythonDir); SetPyvenvCfg(PyRunner.PythonDir);
RunDetached($"-m pip install {args}", outputAction); RunDetached($"-m pip install {args}", outputAction);
await Process.WaitForExitAsync().ConfigureAwait(false); await Process.WaitForExitAsync().ConfigureAwait(false);
// Check return code // Check return code
if (Process.ExitCode != 0) if (Process.ExitCode != 0)
{ {
throw new ProcessException( throw new ProcessException(
$"pip install failed with code {Process.ExitCode}: {output.ToString().ToRepr()}"); $"pip install failed with code {Process.ExitCode}: {output.ToString().ToRepr()}"
);
} }
} }
/// <summary> /// <summary>
/// Run a command using the venv Python executable and return the result. /// Run a command using the venv Python executable and return the result.
/// </summary> /// </summary>
@ -209,17 +227,23 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
{ {
// Record output for errors // Record output for errors
var output = new StringBuilder(); var output = new StringBuilder();
var outputAction = new Action<string?>(s => var outputAction = new Action<string?>(s =>
{ {
if (s == null) return; if (s == null)
return;
Logger.Debug("Pip output: {Text}", s); Logger.Debug("Pip output: {Text}", s);
output.Append(s); output.Append(s);
}); });
SetPyvenvCfg(PyRunner.PythonDir); SetPyvenvCfg(PyRunner.PythonDir);
using var process = ProcessRunner.StartProcess(PythonPath, arguments, using var process = ProcessRunner.StartProcess(
WorkingDirectory?.FullPath, outputAction, EnvironmentVariables); PythonPath,
arguments,
WorkingDirectory?.FullPath,
outputAction,
EnvironmentVariables
);
await process.WaitForExitAsync().ConfigureAwait(false); await process.WaitForExitAsync().ConfigureAwait(false);
return new ProcessResult return new ProcessResult
@ -231,45 +255,55 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
[MemberNotNull(nameof(Process))] [MemberNotNull(nameof(Process))]
public void RunDetached( public void RunDetached(
string arguments, string arguments,
Action<ProcessOutput>? outputDataReceived, Action<ProcessOutput>? outputDataReceived,
Action<int>? onExit = null, Action<int>? onExit = null,
bool unbuffered = true) bool unbuffered = true
)
{ {
if (!Exists()) if (!PythonPath.Exists)
{ {
throw new InvalidOperationException("Venv python process does not exist"); throw new FileNotFoundException("Venv python not found", PythonPath);
} }
SetPyvenvCfg(PyRunner.PythonDir); SetPyvenvCfg(PyRunner.PythonDir);
Logger.Debug($"Launching RunDetached at {PythonPath} with args {arguments}");
var filteredOutput = outputDataReceived == null ? null : new Action<ProcessOutput>(s => Logger.Info(
{ "Launching venv process [{PythonPath}] " +
if (SuppressOutput.Any(s.Text.Contains)) "in working directory [{WorkingDirectory}] with args {arguments.ToRepr()}",
{ PythonPath,
Logger.Info("Filtered output: {S}", s); WorkingDirectory,
return; arguments.ToRepr()
} );
outputDataReceived.Invoke(s);
}); var filteredOutput =
outputDataReceived == null
? null
: new Action<ProcessOutput>(s =>
{
if (SuppressOutput.Any(s.Text.Contains))
{
Logger.Info("Filtered output: {S}", s);
return;
}
outputDataReceived.Invoke(s);
});
var env = new Dictionary<string, string>(); var env = new Dictionary<string, string>();
if (EnvironmentVariables != null) if (EnvironmentVariables != null)
{ {
env.Update(EnvironmentVariables); env.Update(EnvironmentVariables);
} }
// Disable pip caching - uses significant memory for large packages like torch // Disable pip caching - uses significant memory for large packages like torch
env["PIP_NO_CACHE_DIR"] = "true"; env["PIP_NO_CACHE_DIR"] = "true";
// On windows, add portable git // On windows, add portable git
if (Compat.IsWindows) if (Compat.IsWindows)
{ {
var portableGit = GlobalConfig.LibraryDir.JoinDir("PortableGit", "bin"); var portableGit = GlobalConfig.LibraryDir.JoinDir("PortableGit", "bin");
env["PATH"] = Compat.GetEnvPathWithExtensions(portableGit); env["PATH"] = Compat.GetEnvPathWithExtensions(portableGit);
} }
if (unbuffered) if (unbuffered)
{ {
env["PYTHONUNBUFFERED"] = "1"; env["PYTHONUNBUFFERED"] = "1";
@ -286,10 +320,13 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
} }
} }
Process = ProcessRunner.StartAnsiProcess(PythonPath, arguments, Process = ProcessRunner.StartAnsiProcess(
PythonPath,
arguments,
workingDirectory: WorkingDirectory?.FullPath, workingDirectory: WorkingDirectory?.FullPath,
outputDataReceived: filteredOutput, outputDataReceived: filteredOutput,
environmentVariables: env); environmentVariables: env
);
if (onExit != null) if (onExit != null)
{ {
@ -300,7 +337,7 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
}; };
} }
} }
/// <summary> /// <summary>
/// Get entry points for a package. /// Get entry points for a package.
/// https://packaging.python.org/en/latest/specifications/entry-points/#entry-points /// https://packaging.python.org/en/latest/specifications/entry-points/#entry-points
@ -314,7 +351,7 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
results = entry_points(group='console_scripts', name='{entryPointName}') results = entry_points(group='console_scripts', name='{entryPointName}')
print(tuple(results)[0].value, end='') print(tuple(results)[0].value, end='')
"""; """;
var result = await Run($"-c \"{code}\"").ConfigureAwait(false); var result = await Run($"-c \"{code}\"").ConfigureAwait(false);
if (result.ExitCode == 0 && !string.IsNullOrWhiteSpace(result.StandardOutput)) if (result.ExitCode == 0 && !string.IsNullOrWhiteSpace(result.StandardOutput))
{ {
@ -323,7 +360,7 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
return null; return null;
} }
/// <summary> /// <summary>
/// Kills the running process and cancels stream readers, does not wait for exit. /// Kills the running process and cancels stream readers, does not wait for exit.
/// </summary> /// </summary>
@ -335,11 +372,11 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
Process.Kill(); Process.Kill();
Process.Dispose(); Process.Dispose();
} }
Process = null; Process = null;
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
/// <summary> /// <summary>
/// Kills the running process, waits for exit. /// Kills the running process, waits for exit.
/// </summary> /// </summary>

Loading…
Cancel
Save