Skip to content

Commit

Permalink
Merge pull request #110 from tonybaloney/locator_tests
Browse files Browse the repository at this point in the history
Fixes to the Pip installer if more than one version of Python is installed
  • Loading branch information
tonybaloney authored Aug 14, 2024
2 parents fb10964 + c963d6d commit 0079b2b
Show file tree
Hide file tree
Showing 12 changed files with 200 additions and 126 deletions.
11 changes: 9 additions & 2 deletions src/CSnakes.Runtime/Locators/PythonLocationMetadata.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
namespace CSnakes.Runtime.Locators;

/// <summary>
/// Metadata about the location of a Python installation.
/// </summary>
/// <param name="Folder">Path on disk where Python is to be loaded from.</param>
/// <param name="Version">Version of Python being used from the location.</param>
/// <param name="Debug">True if the Python installation is a debug build.</param>
/// <param name="FreeThreaded"
public sealed record PythonLocationMetadata(string Folder, Version Version, bool Debug = false, bool FreeThreaded = false);
public sealed record PythonLocationMetadata(
string Folder,
Version Version,
string LibPythonPath,
string PythonPath,
string PythonBinaryPath,
bool Debug = false,
bool FreeThreaded = false);
77 changes: 74 additions & 3 deletions src/CSnakes.Runtime/Locators/PythonLocator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace CSnakes.Runtime.Locators;
using System.Runtime.InteropServices;

namespace CSnakes.Runtime.Locators;

/// <summary>
/// Abstract class for locating Python installations.
Expand All @@ -20,20 +22,89 @@ public abstract class PythonLocator(Version version)
/// <returns>The metadata of the located Python installation.</returns>
public abstract PythonLocationMetadata LocatePython();

protected string GetPythonExecutablePath(string folder)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return Path.Combine(folder, "python.exe");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return Path.Combine(folder, "bin", "python3");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return Path.Combine(folder, "bin", "python3");
}
else
{
throw new PlatformNotSupportedException("Unsupported platform.");
}
}

protected string GetLibPythonPath(string folder, bool freeThreaded = false)
{
string suffix = freeThreaded ? "t" : "";
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return Path.Combine(folder, $"python{version.Major}{version.Minor}{suffix}.dll");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return Path.Combine(folder, "lib", $"libpython{version.Major}.{version.Minor}{suffix}.dylib");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return Path.Combine(folder, "lib", $"libpython{version.Major}.{version.Minor}{suffix}.so");
}
else
{
throw new PlatformNotSupportedException("Unsupported platform.");
}
}

/// <summary>
/// Get the standard lib path for Python.
/// </summary>
/// <param name="folder">The base folder</param>
/// <returns></returns>
protected string GetPythonPath(string folder)
{
char sep = Path.PathSeparator;

// Add standard library to PYTHONPATH
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return Path.Combine(folder, "Lib") + sep + Path.Combine(folder, "DLLs");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return Path.Combine(folder, "lib", $"python{version.Major}.{version.Minor}") + sep + Path.Combine(folder, "lib", $"python{version.Major}.{version.Minor}", "lib-dynload");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return Path.Combine(folder, "lib", $"python{version.Major}.{version.Minor}") + sep + Path.Combine(folder, "lib", $"python{version.Major}.{version.Minor}", "lib-dynload");
}
else
{
throw new PlatformNotSupportedException("Unsupported platform.");
}
}

/// <summary>
/// Locates the Python installation internally.
/// </summary>
/// <param name="folder">The folder path to search for Python.</param>
/// <returns>The metadata of the located Python installation.</returns>
/// <exception cref="DirectoryNotFoundException">Python not found at the specified folder.</exception>
protected PythonLocationMetadata LocatePythonInternal(string folder)
protected PythonLocationMetadata LocatePythonInternal(string folder, bool freeThreaded = false)
{
if (!Directory.Exists(folder))
{
throw new DirectoryNotFoundException($"Python not found at {folder}.");
}

return new PythonLocationMetadata(folder, Version);
return new PythonLocationMetadata(folder, Version, GetLibPythonPath(folder, freeThreaded), GetPythonPath(folder), GetPythonExecutablePath(folder));
}

/// <summary>
Expand Down
7 changes: 1 addition & 6 deletions src/CSnakes.Runtime/Locators/SourceLocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@ public override PythonLocationMetadata LocatePython()
throw new DirectoryNotFoundException($"Python {Version} not found in {buildFolder}.");
}

return new PythonLocationMetadata(
buildFolder,
Version,
debug,
freeThreaded
);
return LocatePythonInternal(folder, freeThreaded);
}
}
1 change: 1 addition & 0 deletions src/CSnakes.Runtime/PackageManagement/PipInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ internal class PipInstaller(ILogger<PipInstaller> logger) : IPythonPackageInstal

public Task InstallPackages(string home, string? virtualEnvironmentLocation)
{
// TODO:Allow overriding of the requirements file name.
string requirementsPath = Path.Combine(home, "requirements.txt");
if (File.Exists(requirementsPath))
{
Expand Down
87 changes: 30 additions & 57 deletions src/CSnakes.Runtime/PythonEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,24 @@ private PythonEnvironment(
}

string home = options.Home;
string[] extraPaths = options.ExtraPaths;

if (!Directory.Exists(home))
{
logger.LogError("Python home directory does not exist: {Home}", home);
throw new DirectoryNotFoundException("Python home directory does not exist.");
}

if (!string.IsNullOrEmpty(options.VirtualEnvironmentPath)) {
string venvLibPath = string.Empty;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
venvLibPath = Path.Combine(options.VirtualEnvironmentPath, "Lib", "site-packages");
else
venvLibPath = Path.Combine(options.VirtualEnvironmentPath, "lib", $"python{location.Version.Major}.{location.Version.Minor}", "site-packages");
logger.LogDebug("Adding virtual environment site-packages to extra paths: {VenvLibPath}", venvLibPath);
extraPaths = [.. options.ExtraPaths, venvLibPath];
}

if (options.EnsureVirtualEnvironment)
{
EnsureVirtualEnvironment(location, options.VirtualEnvironmentPath);
Expand All @@ -77,10 +88,10 @@ private PythonEnvironment(
api.PythonPath = api.PythonPath + sep + home;
}

if (options.ExtraPaths is { Length: > 0 })
if (extraPaths is { Length: > 0 })
{
logger.LogInformation("Adding extra paths to PYTHONPATH: {ExtraPaths}", options.ExtraPaths);
api.PythonPath = api.PythonPath + sep + string.Join(sep, options.ExtraPaths);
logger.LogInformation("Adding extra paths to PYTHONPATH: {ExtraPaths}", extraPaths);
api.PythonPath = api.PythonPath + sep + string.Join(sep, extraPaths);
}
api.Initialize();
}
Expand All @@ -95,18 +106,26 @@ private void EnsureVirtualEnvironment(PythonLocationMetadata pythonLocation, str

if (!Directory.Exists(venvPath))
{
Logger.LogInformation("Creating virtual environment at {VirtualEnvPath}", venvPath);
Logger.LogInformation("Creating virtual environment at {VirtualEnvPath} using {PythonBinaryPath}", venvPath, pythonLocation.PythonBinaryPath);
using Process process1 = ExecutePythonCommand(pythonLocation, venvPath, $"-VV");
using Process process2 = ExecutePythonCommand(pythonLocation, venvPath, $"-m venv {venvPath}");
}
else
{
Logger.LogInformation("Virtual environment already exists at {VirtualEnvPath}", venvPath);
}

Process ExecutePythonCommand(PythonLocationMetadata pythonLocation, string? venvPath, string arguments)
{
ProcessStartInfo startInfo = new()
{
WorkingDirectory = pythonLocation.Folder,
FileName = "python",
Arguments = $"-m venv {venvPath}"
FileName = pythonLocation.PythonBinaryPath,
Arguments = arguments
};
startInfo.RedirectStandardError = true;
startInfo.RedirectStandardOutput = true;

using Process process = new() { StartInfo = startInfo };
Process process = new() { StartInfo = startInfo };
process.OutputDataReceived += (sender, e) =>
{
if (!string.IsNullOrEmpty(e.Data))
Expand All @@ -127,60 +146,14 @@ private void EnsureVirtualEnvironment(PythonLocationMetadata pythonLocation, str
process.BeginErrorReadLine();
process.BeginOutputReadLine();
process.WaitForExit();
return process;
}
}

private CPythonAPI SetupStandardLibrary(PythonLocationMetadata pythonLocationMetadata, char sep)
{
string pythonDll = string.Empty;
string pythonPath = string.Empty;
string pythonLocation = pythonLocationMetadata.Folder;
var version = pythonLocationMetadata.Version;
string suffix = string.Empty;

if (pythonLocationMetadata.FreeThreaded)
{
suffix += "t";
}

// Add standard library to PYTHONPATH
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
suffix += pythonLocationMetadata.Debug ? "_d" : string.Empty;
pythonDll = Path.Combine(pythonLocation, $"python{version.Major}{version.Minor}{suffix}.dll");
if (pythonLocationMetadata.Debug)
{
// From source..
pythonPath = Path.Combine(pythonLocation, "..", "..", "Lib") + sep + pythonLocation;
}
else
{
pythonPath = Path.Combine(pythonLocation, "Lib") + sep + Path.Combine(pythonLocation, "DLLs");
}
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
suffix += pythonLocationMetadata.Debug ? "d" : string.Empty;
if (pythonLocationMetadata.Debug) // from source
{
pythonDll = Path.Combine(pythonLocation, $"libpython{version.Major}.{version.Minor}{suffix}.dylib");
pythonPath = Path.Combine(pythonLocation, "Lib"); // TODO : build/lib.macosx-13.6-x86_64-3.13-pydebug
}
else
{
pythonDll = Path.Combine(pythonLocation, "lib", $"libpython{version.Major}.{version.Minor}{suffix}.dylib");
pythonPath = Path.Combine(pythonLocation, "lib", $"python{version.Major}.{version.Minor}") + sep + Path.Combine(pythonLocation, "lib", $"python{version.Major}.{version.Minor}", "lib-dynload");
}
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
pythonDll = Path.Combine(pythonLocation, "lib", $"libpython{version.Major}.{version.Minor}{suffix}.so");
pythonPath = Path.Combine(pythonLocation, "lib", $"python{version.Major}.{version.Minor}") + sep + Path.Combine(pythonLocation, "lib", $"python{version.Major}.{version.Minor}", "lib-dynload");
}
else
{
throw new PlatformNotSupportedException("Unsupported platform.");
}
string pythonDll = pythonLocationMetadata.LibPythonPath;
string pythonPath = pythonLocationMetadata.PythonPath;

Logger.LogInformation("Python DLL: {PythonDLL}", pythonDll);
Logger.LogInformation("Python path: {PythonPath}", pythonPath);
Expand Down
4 changes: 1 addition & 3 deletions src/CSnakes.Runtime/PythonEnvironmentBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ internal partial class PythonEnvironmentBuilder(IServiceCollection services) : I
{
private bool ensureVirtualEnvironment = false;
private string? virtualEnvironmentLocation;
private string[] extraPaths = [];
private readonly string[] extraPaths = [];
private string home = Environment.CurrentDirectory;

public IServiceCollection Services { get; } = services;
Expand All @@ -15,8 +15,6 @@ public IPythonEnvironmentBuilder WithVirtualEnvironment(string path, bool ensure
{
this.ensureVirtualEnvironment = ensureVirtualEnvironment;
virtualEnvironmentLocation = path;
extraPaths = [.. extraPaths, path, Path.Combine(virtualEnvironmentLocation, "Lib", "site-packages")];

return this;
}

Expand Down
Loading

0 comments on commit 0079b2b

Please sign in to comment.