diff --git a/src/CSnakes.Runtime/Locators/PythonLocationMetadata.cs b/src/CSnakes.Runtime/Locators/PythonLocationMetadata.cs index 364d9a5d..f1c8d293 100644 --- a/src/CSnakes.Runtime/Locators/PythonLocationMetadata.cs +++ b/src/CSnakes.Runtime/Locators/PythonLocationMetadata.cs @@ -1,9 +1,16 @@ namespace CSnakes.Runtime.Locators; + /// /// Metadata about the location of a Python installation. /// /// Path on disk where Python is to be loaded from. /// Version of Python being used from the location. /// True if the Python installation is a debug build. -/// /// Abstract class for locating Python installations. @@ -20,20 +22,89 @@ public abstract class PythonLocator(Version version) /// The metadata of the located Python installation. 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."); + } + } + + /// + /// Get the standard lib path for Python. + /// + /// The base folder + /// + 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."); + } + } + /// /// Locates the Python installation internally. /// /// The folder path to search for Python. /// The metadata of the located Python installation. /// Python not found at the specified folder. - 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)); } /// diff --git a/src/CSnakes.Runtime/Locators/SourceLocator.cs b/src/CSnakes.Runtime/Locators/SourceLocator.cs index c6f7dbe5..e55857b3 100644 --- a/src/CSnakes.Runtime/Locators/SourceLocator.cs +++ b/src/CSnakes.Runtime/Locators/SourceLocator.cs @@ -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); } } diff --git a/src/CSnakes.Runtime/PackageManagement/PipInstaller.cs b/src/CSnakes.Runtime/PackageManagement/PipInstaller.cs index e10f747a..bd2891eb 100644 --- a/src/CSnakes.Runtime/PackageManagement/PipInstaller.cs +++ b/src/CSnakes.Runtime/PackageManagement/PipInstaller.cs @@ -9,6 +9,7 @@ internal class PipInstaller(ILogger 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)) { diff --git a/src/CSnakes.Runtime/PythonEnvironment.cs b/src/CSnakes.Runtime/PythonEnvironment.cs index 1d833eea..ee2fbc85 100644 --- a/src/CSnakes.Runtime/PythonEnvironment.cs +++ b/src/CSnakes.Runtime/PythonEnvironment.cs @@ -49,6 +49,7 @@ private PythonEnvironment( } string home = options.Home; + string[] extraPaths = options.ExtraPaths; if (!Directory.Exists(home)) { @@ -56,6 +57,16 @@ private PythonEnvironment( 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); @@ -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(); } @@ -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)) @@ -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); diff --git a/src/CSnakes.Runtime/PythonEnvironmentBuilder.cs b/src/CSnakes.Runtime/PythonEnvironmentBuilder.cs index 7ecb2659..66812239 100644 --- a/src/CSnakes.Runtime/PythonEnvironmentBuilder.cs +++ b/src/CSnakes.Runtime/PythonEnvironmentBuilder.cs @@ -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; @@ -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; } diff --git a/src/Integration.Tests/BasicTests.cs b/src/Integration.Tests/BasicTests.cs index bb645592..c8c7d2a6 100644 --- a/src/Integration.Tests/BasicTests.cs +++ b/src/Integration.Tests/BasicTests.cs @@ -1,54 +1,54 @@ -namespace Integration.Tests; - -public class BasicTests : IntegrationTestBase -{ - [Fact] - public void TestBasic_TestIntFloat() - { - var testModule = Env.TestBasic(); - Assert.Equal(4.3, testModule.TestIntFloat(4, 0.3)); - } - - [Fact] - public void TestBasic_TestFloatInt() - { - var testModule = Env.TestBasic(); - Assert.Equal(4.3, testModule.TestFloatInt(0.3, 4)); - } - - [Fact] - public void TestBasic_TestFloatFloat() - { - var testModule = Env.TestBasic(); - Assert.Equal(4.3, testModule.TestFloatFloat(0.3, 4.0)); - } - - [Fact] - public void TestBasic_TestIntInt() - { - var testModule = Env.TestBasic(); - Assert.Equal(6, testModule.TestIntInt(4, 2)); - } - - [Fact] - public void TestBasic_TestListOfInts() - { - var testModule = Env.TestBasic(); - Assert.Equal([1, 2, 3], testModule.TestListOfInts([1, 2, 3])); - } - - [Fact] - public void TestBasic_TestTwoStrings() - { - var testModule = Env.TestBasic(); - Assert.Equal("hello w0rld", testModule.TestTwoStrings("hello ", "w0rld")); - } - - [Fact] - public void TestBasic_TestTwoListsOfStrings() - { - var testModule = Env.TestBasic(); - Assert.Equal(["h3llo", "worLd", "this", "is", "a", "test"], testModule.TestTwoListsOfStrings(["h3llo", "worLd"], new string[] { "this", "is", "a", "test" })); +namespace Integration.Tests; + +public class BasicTests : IntegrationTestBase +{ + [Fact] + public void TestBasic_TestIntFloat() + { + var testModule = Env.TestBasic(); + Assert.Equal(4.3, testModule.TestIntFloat(4, 0.3)); + } + + [Fact] + public void TestBasic_TestFloatInt() + { + var testModule = Env.TestBasic(); + Assert.Equal(4.3, testModule.TestFloatInt(0.3, 4)); + } + + [Fact] + public void TestBasic_TestFloatFloat() + { + var testModule = Env.TestBasic(); + Assert.Equal(4.3, testModule.TestFloatFloat(0.3, 4.0)); + } + + [Fact] + public void TestBasic_TestIntInt() + { + var testModule = Env.TestBasic(); + Assert.Equal(6, testModule.TestIntInt(4, 2)); + } + + [Fact] + public void TestBasic_TestListOfInts() + { + var testModule = Env.TestBasic(); + Assert.Equal([1, 2, 3], testModule.TestListOfInts([1, 2, 3])); + } + + [Fact] + public void TestBasic_TestTwoStrings() + { + var testModule = Env.TestBasic(); + Assert.Equal("hello w0rld", testModule.TestTwoStrings("hello ", "w0rld")); + } + + [Fact] + public void TestBasic_TestTwoListsOfStrings() + { + var testModule = Env.TestBasic(); + Assert.Equal(["h3llo", "worLd", "this", "is", "a", "test"], testModule.TestTwoListsOfStrings(["h3llo", "worLd"], new string[] { "this", "is", "a", "test" })); } [Fact] @@ -56,5 +56,5 @@ public void TestBasic_TestBytes() { var testModule = Env.TestBasic(); Assert.Equal(new byte[] { 0x04, 0x03, 0x02, 0x01 }, testModule.TestBytes(new byte[] { 0x01, 0x02, 0x03, 0x04 })); - } -} + } +} diff --git a/src/Integration.Tests/Integration.Tests.csproj b/src/Integration.Tests/Integration.Tests.csproj index c075e40a..03570930 100644 --- a/src/Integration.Tests/Integration.Tests.csproj +++ b/src/Integration.Tests/Integration.Tests.csproj @@ -7,8 +7,10 @@ + + @@ -24,6 +26,9 @@ Always + + Always + Always @@ -57,7 +62,7 @@ - + @@ -66,6 +71,9 @@ + + Always + diff --git a/src/Integration.Tests/IntegrationTestBase.cs b/src/Integration.Tests/IntegrationTestBase.cs index 82a1fb6c..0713b893 100644 --- a/src/Integration.Tests/IntegrationTestBase.cs +++ b/src/Integration.Tests/IntegrationTestBase.cs @@ -22,7 +22,9 @@ public IntegrationTestBase() pb.FromNuGet(pythonVersionWindows) .FromMacOSInstallerLocator(pythonVersionMacOS) - .FromEnvironmentVariable("Python3_ROOT_DIR", pythonVersionLinux); // This last one is for GitHub Actions + .FromEnvironmentVariable("Python3_ROOT_DIR", pythonVersionLinux) + .WithVirtualEnvironment(Path.Join(Environment.CurrentDirectory, "python", ".venv")) + .WithPipInstaller(); services.AddLogging(builder => builder.AddXUnit()); }) diff --git a/src/Integration.Tests/TestDependency.cs b/src/Integration.Tests/TestDependency.cs new file mode 100644 index 00000000..54665b9f --- /dev/null +++ b/src/Integration.Tests/TestDependency.cs @@ -0,0 +1,14 @@ +using Integration.Tests; +using System.Runtime.InteropServices; + +public class TestDependency : IntegrationTestBase +{ + [Fact] + public void VerifyInstalledPackage() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) // TODO: Fix virtual environments on Linux + { + Assert.True(Env.TestDependency().TestNothing()); + } + } +} diff --git a/src/Integration.Tests/python/requirements.txt b/src/Integration.Tests/python/requirements.txt new file mode 100644 index 00000000..79228389 --- /dev/null +++ b/src/Integration.Tests/python/requirements.txt @@ -0,0 +1 @@ +httpx \ No newline at end of file diff --git a/src/Integration.Tests/python/test_dependency.py b/src/Integration.Tests/python/test_dependency.py new file mode 100644 index 00000000..8d77968d --- /dev/null +++ b/src/Integration.Tests/python/test_dependency.py @@ -0,0 +1,4 @@ +import httpx + +def test_nothing() -> bool: + return True