Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Python.Deployment/DownloadInstallationSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public class DownloadInstallationSource : InstallationSource
/// </summary>
public string DownloadUrl { get; set; }

public override async Task<string> RetrievePythonZip(string destinationDirectory)
public override async Task<string> RetrievePythonZip(string destinationDirectory, Action<float> progress = null, CancellationToken token = default)
{
var zipFile = Path.Combine(destinationDirectory, GetPythonZipFileName());
if (!Force && File.Exists(zipFile))
Expand All @@ -53,7 +53,7 @@ public override async Task<string> RetrievePythonZip(string destinationDirectory
try
{
Log("Downloading source...");
await Downloader.Download(DownloadUrl, zipFile, progress => Log($"{progress:F2}%")).ConfigureAwait(false);
await Downloader.Download(DownloadUrl, zipFile, p => { Log($"{p:F2}%"); progress?.Invoke(p); }, token).ConfigureAwait(false);
Log("Done!");
return zipFile;
}
Expand Down
3 changes: 2 additions & 1 deletion Python.Deployment/EmbeddedResourceInstallationSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Python.Deployment
Expand All @@ -51,7 +52,7 @@ public class EmbeddedResourceInstallationSource : InstallationSource
/// </summary>
public string ResourceName { get; set; }

public override Task<string> RetrievePythonZip(string destinationDirectory)
public override Task<string> RetrievePythonZip(string destinationDirectory, Action<float> progress = null, CancellationToken token = default)
{
var filePath = Path.Combine(destinationDirectory, ResourceName);
if (!Force && File.Exists(filePath))
Expand Down
3 changes: 2 additions & 1 deletion Python.Deployment/InstallationSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

namespace Python.Deployment
Expand All @@ -46,7 +47,7 @@ public abstract class InstallationSource
/// </summary>
/// <param name="destinationDirectory">The directory location where the retrieved zip file should be placed</param>
/// <returns></returns>
public abstract Task<string> RetrievePythonZip(string destinationDirectory);
public abstract Task<string> RetrievePythonZip(string destinationDirectory, Action<float> progress = null, CancellationToken token = default);

/// <summary>
/// If true, retrieve the python file again even if it already exists at the destination path
Expand Down
148 changes: 135 additions & 13 deletions Python.Deployment/Installer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

Expand Down Expand Up @@ -73,12 +74,12 @@ private static void Log(string message)
LogMessage?.Invoke(message);
}

public static async Task SetupPython(bool force = false)
public static async Task SetupPython(Action<float> progress = null, CancellationToken token = default, bool force = false)
{
Environment.SetEnvironmentVariable("PATH", $"{EmbeddedPythonHome};" + Environment.GetEnvironmentVariable("PATH"));
if (!force && Directory.Exists(EmbeddedPythonHome) && File.Exists(Path.Combine(EmbeddedPythonHome, "python.exe"))) // python seems installed, so exit
return;
var zip = await Source.RetrievePythonZip(InstallPath).ConfigureAwait(false);
var zip = await Source.RetrievePythonZip(InstallPath, progress, token).ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(zip))
{
Log("SetupPython: Error obtaining zip file from installation source");
Expand Down Expand Up @@ -242,7 +243,7 @@ public static async Task PipInstallWheel(Assembly assembly, string resource_name

CopyEmbeddedResourceToFile(assembly, key, wheelPath, force);

await TryInstallPip().ConfigureAwait(false);
await TryInstallPip(token: token).ConfigureAwait(false);

await RunCommand($"\"{pipPath}\" install \"{wheelPath}\"", token).ConfigureAwait(false);
}
Expand Down Expand Up @@ -289,19 +290,21 @@ public static string GetResourceKey(Assembly assembly, string embedded_file)
/// terminate when complete. When true, the command window must be manually closed before
/// processing will continue.
/// </param>
public static async Task PipInstallModule(string module_name, string version = "", bool force = false, CancellationToken token = default)
public static async Task PipInstallModule(string module_name, string version = "", bool force = false, Action<float> progress = null, CancellationToken token = default)
{
await TryInstallPip().ConfigureAwait(false);
await TryInstallPip(progress, token, force).ConfigureAwait(false);

if (IsModuleInstalled(module_name) && !force)
return;

string pipPath = Path.Combine(EmbeddedPythonHome, "Scripts", "pip");
string forceInstall = force ? " --force-reinstall" : "";
if (version.Length > 0)
version = $"=={version}";

await RunCommand($"\"{pipPath}\" install \"{module_name}{version}\" {forceInstall}", token).ConfigureAwait(false);
await RunPipCommand(
$"-u -m pip install \"{module_name}{version}\" --no-cache-dir --progress-bar raw{forceInstall}",
token,
progress).ConfigureAwait(false);
}

/// <summary>
Expand All @@ -315,7 +318,7 @@ public static async Task PipInstallModule(string module_name, string version = "
/// terminate when complete. When true, the command window must be manually closed before
/// processing will continue.
/// </param>
public static async Task InstallPip(CancellationToken token = default)
public static async Task InstallPip(Action<float> progress = null, CancellationToken token = default)
{
string libDir = Path.Combine(EmbeddedPythonHome, "Lib");

Expand All @@ -328,7 +331,7 @@ public static async Task InstallPip(CancellationToken token = default)
try
{
Log("Downloading Pip...");
await Downloader.Download(getPipUrl, getPipFilePath, progress => Log($"{progress:F2}%")).ConfigureAwait(false);
await Downloader.Download(getPipUrl, getPipFilePath, p => { Log($"{p:F2}%"); progress?.Invoke(p); }).ConfigureAwait(false);
Log("Done!");
}
catch (Exception ex)
Expand All @@ -337,17 +340,16 @@ public static async Task InstallPip(CancellationToken token = default)
return;
}


await RunCommand($"cd \"{EmbeddedPythonHome}\" && python.exe Lib\\get-pip.py", token).ConfigureAwait(false);
}

public static async Task<bool> TryInstallPip(bool force = false)
public static async Task<bool> TryInstallPip(Action<float> progress = null, CancellationToken token = default, bool force = false)
{
if (!IsPipInstalled() || force)
{
try
{
await InstallPip().ConfigureAwait(false);
await InstallPip(progress, token).ConfigureAwait(false);
}
catch
{
Expand Down Expand Up @@ -449,6 +451,126 @@ public static async Task RunCommand(string command, CancellationToken token)
}
}

private static async Task RunPipCommand(string pipArguments, CancellationToken token, Action<float> progress = null)
{
Process process = new Process();
try
{
var pythonPath = Path.Combine(EmbeddedPythonHome, "python.exe");

Log($"> \"{pythonPath}\" {pipArguments}");

var startInfo = new ProcessStartInfo
{
FileName = pythonPath,
WorkingDirectory = EmbeddedPythonHome,
Arguments = pipArguments,
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardInput = true,
RedirectStandardOutput = true,
WindowStyle = ProcessWindowStyle.Hidden,
};

// pip-specific output/format settings
startInfo.Environment["PYTHONUNBUFFERED"] = "1";
startInfo.Environment["PYTHONIOENCODING"] = "utf-8";
startInfo.Environment["PIP_NO_COLOR"] = "1";
startInfo.Environment["COLUMNS"] = "200";

process.StartInfo = startInfo;
process.Start();

token.Register(() =>
{
try { if (!process.HasExited) process.Kill(); }
catch (Exception) { /* ignore */ }
});

var readStdOut = ReadStreamAsync(process.StandardOutput, line =>
{
Log($"[OUT] '{line}'");
ParsePipProgress(line, progress);
}, token);

var readStdErr = ReadStreamAsync(process.StandardError, line =>
{
Log($"[ERR] '{line}'");
Console.WriteLine(line);
}, token);

await Task.WhenAll(readStdOut, readStdErr).ConfigureAwait(false);
await Task.Run(() => process.WaitForExit(), token).ConfigureAwait(false);

if (process.ExitCode == 0)
progress?.Invoke(100.0f);

Log(" => exit code " + process.ExitCode);
}
catch (OperationCanceledException)
{
Log("RunPipCommand: Cancelled");
}
catch (Exception e)
{
Log($"RunPipCommand: Error with arguments: '{pipArguments}'\r\n{e.Message}");
}
finally
{
process?.Dispose();
}
}

private static async Task ReadStreamAsync(StreamReader reader, Action<string> callback, CancellationToken token)
{
var sb = new System.Text.StringBuilder();
char[] buf = new char[1];

while (!reader.EndOfStream && !token.IsCancellationRequested)
{
int read = await reader.ReadAsync(buf, 0, 1).ConfigureAwait(false);
if (read == 0) break;

char c = buf[0];
if (c == '\n' || c == '\r')
{
if (sb.Length > 0)
{
callback(sb.ToString());
sb.Clear();
}
}
else
{
sb.Append(c);
}
}

if (sb.Length > 0)
callback(sb.ToString());
}

private static void ParsePipProgress(string line, Action<float> progress)
{
if (progress == null) return;

var match = System.Text.RegularExpressions.Regex.Match(
line, @"Progress (\d+) of (\d+)"
);

if (match.Success)
{
if (long.TryParse(match.Groups[1].Value, out long current) &&
long.TryParse(match.Groups[2].Value, out long total) &&
total > 0)
{
float percent = (float)current / total * 100f;
progress(Math.Min(Math.Max(percent, 0f), 99f));
}
}
}

private static bool AreAllFilesAlreadyPresent(ZipArchive zip, string lib)
{
var allFilesAllReadyPresent = true;
Expand All @@ -465,4 +587,4 @@ private static bool AreAllFilesAlreadyPresent(ZipArchive zip, string lib)
return allFilesAllReadyPresent;
}
}
}
}
16 changes: 8 additions & 8 deletions Python.Included/Installer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ private static void Log(string message)
LogMessage?.Invoke(message);
}

public static async Task SetupPython(bool force = false)
public static async Task SetupPython(Action<float> progress = null, CancellationToken token = default, bool force = false)
{
if (!PythonEnv.DeployEmbeddedPython)
return;
Expand All @@ -75,7 +75,7 @@ public static async Task SetupPython(bool force = false)
Python.Deployment.Installer.Source = GetInstallationSource();
Python.Deployment.Installer.PythonDirectoryName = InstallDirectory;
Python.Deployment.Installer.InstallPath = InstallPath;
await Python.Deployment.Installer.SetupPython(force).ConfigureAwait(false);
await Python.Deployment.Installer.SetupPython(progress, token, force).ConfigureAwait(false);
}
finally
{
Expand Down Expand Up @@ -149,15 +149,15 @@ public static async Task PipInstallWheel(Assembly assembly, string resource_name
/// </summary>
/// <param name="module_name">The module/package to install </param>
/// <param name="force">When true, reinstall the packages even if it is already up-to-date.</param>
public static async Task PipInstallModule(string module_name, string version = "", bool force = false, CancellationToken token = default)
public static async Task PipInstallModule(string module_name, string version = "", bool force = false, Action<float> progress = null, CancellationToken token = default)
{
try
{
Python.Deployment.Installer.LogMessage += Log;
Python.Deployment.Installer.Source = GetInstallationSource();
Python.Deployment.Installer.PythonDirectoryName = InstallDirectory;
Python.Deployment.Installer.InstallPath = InstallPath;
await Python.Deployment.Installer.PipInstallModule(module_name, version, force, token).ConfigureAwait(false);
await Python.Deployment.Installer.PipInstallModule(module_name, version, force, progress, token).ConfigureAwait(false);
}
finally
{
Expand All @@ -171,29 +171,29 @@ public static async Task PipInstallModule(string module_name, string version = "
/// <remarks>
/// Creates the lib folder under <see cref="EmbeddedPythonHome"/> if it does not exist.
/// </remarks>
public static async Task InstallPip(CancellationToken token = default)
public static async Task InstallPip(Action<float> progress = null, CancellationToken token = default)
{
try
{
Python.Deployment.Installer.LogMessage += Log;
Python.Deployment.Installer.Source = GetInstallationSource();
Python.Deployment.Installer.PythonDirectoryName = InstallDirectory;
Python.Deployment.Installer.InstallPath = InstallPath;
await Python.Deployment.Installer.InstallPip(token).ConfigureAwait(false);
await Python.Deployment.Installer.InstallPip(progress, token).ConfigureAwait(false);
}
finally
{
Python.Deployment.Installer.LogMessage -= Log;
}
}

public static async Task<bool> TryInstallPip(bool force = false)
public static async Task<bool> TryInstallPip(Action<float> progress = null, CancellationToken token = default, bool force = false)
{
if (!IsPipInstalled() || force)
{
try
{
await InstallPip().ConfigureAwait(false);
await InstallPip(progress, token).ConfigureAwait(false);
}
catch
{
Expand Down