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