Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor to use the windows installer when creating a base environment #145

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7fa170b
initial solution for the refactor. python installer now used instead …
Tom-Kingstone Sep 5, 2024
2d61d79
rename the python versions to be just the major numbers to avoid conf…
Tom-Kingstone Sep 5, 2024
bbb5621
fixed remove method
Tom-Kingstone Sep 9, 2024
badc7a9
fixed base environment install location
Tom-Kingstone Sep 9, 2024
15d41fe
use DirectoryBaseEnvironment properly everywhere
Tom-Kingstone Sep 9, 2024
1131f9e
added bhom analytics logger, and added the interpreter path (sys.exec…
Tom-Kingstone Sep 13, 2024
09247c4
changed log entry type to be the same as the one in BHoM
Tom-Kingstone Sep 13, 2024
604532a
add method separator for Download.cs
Tom-Kingstone Oct 1, 2024
a674209
update versioning for removed/updated methods.
Tom-Kingstone Oct 1, 2024
01eb894
fix versioning keys
Tom-Kingstone Oct 1, 2024
4413c31
rename versioning file
Tom-Kingstone Oct 1, 2024
162cf42
updated wrong versioning key :P
Tom-Kingstone Oct 1, 2024
895c4e8
fix key for DownloadGetPip
Tom-Kingstone Oct 1, 2024
9fc085a
updated DirectoryBaseEnvironment and BasePythonEnvironment
Tom-Kingstone Oct 1, 2024
fc0513d
add local package to base environment on install
Tom-Kingstone Oct 1, 2024
db51afc
mistake in method signature
Tom-Kingstone Oct 1, 2024
71a1a7c
general compliance changes
Tom-Kingstone Oct 1, 2024
f1b93e8
revert ToPython removal (versioning doesn't play nice)
Tom-Kingstone Oct 2, 2024
642f2f5
Update Python_Engine/Compute/BasePythonEnvironment.cs
Tom-Kingstone Oct 2, 2024
660a338
Made the warning for creating virtual environments clearer (need the …
Tom-Kingstone Oct 22, 2024
631ad9e
Removed error if the environment didn't exist - now downloads the req…
Tom-Kingstone Oct 29, 2024
6561cb0
add a comment and if spacing
Tom-Kingstone Oct 29, 2024
3ba64bb
properly install the base environment when asked for
Tom-Kingstone Oct 29, 2024
37eb521
change to run installer check again
Tom-Kingstone Oct 30, 2024
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
55 changes: 11 additions & 44 deletions Python_Engine/Compute/BasePythonEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,14 @@ namespace BH.Engine.Python
{
public static partial class Compute
{
[PreviousVersion("8.0", "BH.Engine.Python.Compute.BasePythonEnvironment(System.Boolean, System.Boolean)")]
[Description("Retrieve or reinstall the base Python Environment for BHoM workflows.")]
[Input("version", "The target version of python to be installed or retrieved.")]
[Input("reload", "Reload the base Python environment rather than recreating it, if it already exists.")]
[Input("run", "Start the installation/retrieval of the BHoM Base Python Environment.")]
[Output("env", "The base Python Environment for all BHoM workflows.")]
public static PythonEnvironment BasePythonEnvironment(
PythonVersion version = PythonVersion.v3_10,
bool reload = true,
bool run = false
)
Expand All @@ -52,10 +55,10 @@ public static PythonEnvironment BasePythonEnvironment(
// create PythonEnvironments directory if it doesnt already exist
Directory.CreateDirectory(Query.DirectoryEnvironments());
}

// determine whether the base environment already exists
string targetExecutable = Path.Combine(Query.DirectoryBaseEnvironment(), "python.exe");
bool exists = Directory.Exists(Query.DirectoryBaseEnvironment()) && File.Exists(targetExecutable);
string targetExecutable = Path.Combine(Query.DirectoryBaseEnvironment(version), "python.exe");
bool exists = File.Exists(targetExecutable);

if (exists && reload)
return new PythonEnvironment() { Name = Query.ToolkitName(), Executable = targetExecutable };
Expand All @@ -64,50 +67,14 @@ public static PythonEnvironment BasePythonEnvironment(
// remove all existing environments and kernels
RemoveEverything();

// download the target Python version and convert into a "full" python installation bypassing admin rights
string executable = PythonVersion.v3_10_5.DownloadPython(Query.ToolkitName());
string pipInstaller = DownloadGetPip(Path.GetDirectoryName(executable));
string baseEnvironmentDirectory = Path.GetDirectoryName(executable);

// install pip into the python installation
Process process = new Process()
{
StartInfo = new ProcessStartInfo()
{
FileName = Modify.AddQuotesIfRequired(executable),
Arguments = Modify.AddQuotesIfRequired(pipInstaller) + " --no-warn-script-location",
RedirectStandardError=true,
UseShellExecute=false,
}
};
using (Process p = Process.Start(process.StartInfo))
{
string standardError = p.StandardError.ReadToEnd();
p.WaitForExit();
if (p.ExitCode != 0)
BH.Engine.Base.Compute.RecordError($"Error installing pip.\n{standardError}");
File.Delete(pipInstaller);
}

// delete files with the suffix ._pth from installedDirectory
List<string> pthFiles = Directory.GetFiles(baseEnvironmentDirectory, "*.*", SearchOption.TopDirectoryOnly).Where(s => s.EndsWith("._pth")).ToList();
foreach (string pthFile in pthFiles)
{
File.Delete(pthFile);
}

// move files with the suffix .dll and .pyd from installedDirectory into a DLLs directory
string libDirectory = Directory.CreateDirectory(Path.Combine(baseEnvironmentDirectory, "DLLs")).FullName;
List<string> libFiles = Directory.GetFiles(baseEnvironmentDirectory, "*.*", SearchOption.TopDirectoryOnly).Where(s => (s.EndsWith(".dll") || s.EndsWith(".pyd")) && !Path.GetFileName(s).Contains("python") && !Path.GetFileName(s).Contains("vcruntime")).ToList();
foreach (string libFile in libFiles)
{
File.Move(libFile, Path.Combine(libDirectory, Path.GetFileName(libFile)));
}
// download and run the installer for the target Python version
string exe = version.DownloadPythonVersion();

// install essential packages into base environment
InstallPackages(executable, new List<string>() { "virtualenv", "jupyterlab", "black", "pylint" });
InstallPackages(exe, new List<string>() { "virtualenv", "jupyterlab", "black", "pylint" });
InstallPackageLocal(exe, Path.Combine(Query.DirectoryCode(), Query.ToolkitName()));

return new PythonEnvironment() { Name = Query.ToolkitName(), Executable = executable };
return new PythonEnvironment() { Name = Query.ToolkitName(), Executable = exe };
}
}
}
Expand Down
71 changes: 46 additions & 25 deletions Python_Engine/Compute/Download.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
using BH.oM.Python.Enums;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Xml.Linq;

namespace BH.Engine.Python
Expand Down Expand Up @@ -85,34 +87,53 @@
return filePath;
}

// TODO - THIS METHOD HAS CHANGED BUT IS STILL USED, SO NEEDS DEPRECATING
// changed from what to what ?
[Description("Download the target version of Python.")]
[Input("version", "A Python version.")]
[Input("name", "Name of target exe file.")]
[Output("executablePath", "The path of the executable for the downloaded Python.")]
public static string DownloadPython(this PythonVersion version, string name = null)
/******************************************************/

[PreviousVersion("8.0", "BH.Engine.Python.Compute.DownloadPython(BH.oM.Python.Enums.PythonVersion, System.String)")]
[Description("Download and install a specified version of python, and return the executable for it.")]
[Input("version", "The version of python to download.")]
[Output("pythonExecutable", "The executable (python.exe) for the python version that was installed")]

Check warning on line 95 in Python_Engine/Compute/Download.cs

View check run for this annotation

BHoMBot-CI / documentation-compliance

Python_Engine/Compute/Download.cs#L95

Documentation attribute should end with grammatically correct punctuation (., !, or ?) - For more information see https://bhom.xyz/documentation/DevOps/Code%20Compliance%20and%20CI/Compliance%20Checks/AttributeHasEndingPunctuation
public static string DownloadPythonVersion(this PythonVersion version)
{
string url = version.EmbeddableURL();
if (string.IsNullOrEmpty(name))
name = Path.GetFileNameWithoutExtension(url);
string targetExecutable = Path.Combine(Query.DirectoryEnvironments(), name, "python.exe");

if (File.Exists(targetExecutable))
return targetExecutable;

string zipfile = DownloadFile(url, Query.DirectoryEnvironments());
UnzipFile(zipfile, Query.DirectoryEnvironments(), name, true);

return targetExecutable;
}

[Description("Download the pip installer")]
[Input("targetDirectory", "The directory into which get-pip.py will be downloaded.")]
[Output("getpipPath", "The path of the file used to install pip into an embedded Python environment.")]
public static string DownloadGetPip(string targetDirectory)
{
return DownloadFile("https://bootstrap.pypa.io/get-pip.py", targetDirectory);
string basePath = Path.Combine(Query.DirectoryBaseEnvironment(version));

if (File.Exists(Path.Combine(basePath, "python.exe")))
return Path.Combine(basePath, "python.exe");

if (!Directory.Exists(basePath))
Directory.CreateDirectory(basePath);
else
{
Directory.Delete(basePath, true); //if there are any files here already for some reason, remove them.
Directory.CreateDirectory(basePath);
}

string installerFile = DownloadFile(url, basePath, "installer.exe");

using (Process install = new Process()
{
StartInfo = new ProcessStartInfo()
{
FileName = installerFile,
Arguments = $"/passive InstallAllUsers=0 InstallLauncherAllUsers=0 Include_launcher=0 Shortcuts=0 AssociateFiles=0 Include_tools=0 Include_test=0 TargetDir={Modify.AddQuotesIfRequired(basePath)}",
RedirectStandardError = true,
UseShellExecute = false,
}
})
{
install.Start();
string stderr = install.StandardError.ReadToEnd();
install.WaitForExit();
if (install.ExitCode != 0)
{
BH.Engine.Base.Compute.RecordError($"Error installing python: {stderr}");
return null;
}
}

return Path.Combine(basePath, "python.exe");
}
}
}
Expand Down
55 changes: 50 additions & 5 deletions Python_Engine/Compute/Remove.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@
*/

using BH.oM.Base.Attributes;
using BH.oM.Python.Enums;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;

namespace BH.Engine.Python
Expand Down Expand Up @@ -77,19 +81,60 @@ public static void RemoveAllVirtualEnvironments()
}
}

[Description("Completely remove the base BHoM Python environment.")]
public static void RemoveBaseEnvironment()
[PreviousVersion("8.0", "BH.Engine.Python.Compute.RemoveBaseEnvironment()")]
[Description("Remove the base install for the python version specified.")]
[Input("version", "The base python version to remove.")]
public static void RemoveBaseVersion(PythonVersion version = PythonVersion.v3_10)
{
string basePath = Path.Combine(Query.DirectoryEnvironments(), Query.ToolkitName());
if (Directory.Exists(basePath))
string basePath = Path.Combine(Query.DirectoryEnvironments(), Query.ToolkitName(), version.ToString());
string baseInstaller = Path.Combine(basePath, "installer.exe");

// If the installer does not exist and the folder does - assume a bad/invalid install and just delete the entire folder.
if (!File.Exists(baseInstaller) && Directory.Exists(basePath))
{
Directory.Delete(basePath, true);
return;
}
else if (!Directory.Exists(basePath))
// If the base path doesn't exist there is nothing to remove.
return;

using (Process uninstall = new Process()
{
StartInfo = new ProcessStartInfo()
{
FileName = baseInstaller,
Arguments = "/uninstall /passive",
RedirectStandardError = true,
UseShellExecute = false,
}
})
{
uninstall.Start();
string stderr = uninstall.StandardError.ReadToEnd();
uninstall.WaitForExit();
if (uninstall.ExitCode != 0)
{
BH.Engine.Base.Compute.RecordError($"Error uninstalling python: {stderr}");
return;
}
}

// Finally remove base folder as the installer may have missed something.
Directory.Delete(basePath, true);
}

[Description("Completely remove all BHoM-related Python environments and kernels.")]
public static void RemoveEverything()
{
RemoveAllVirtualEnvironments();
RemoveBaseEnvironment();

//remove all python versions installed in base directory.
foreach (PythonVersion e in Enum.GetValues(typeof(PythonVersion)))
{
if (e != PythonVersion.Undefined)
RemoveBaseVersion(e);
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions Python_Engine/Compute/UnzipFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ namespace BH.Engine.Python
{
public static partial class Compute
{
//This method is no longer used by python toolkit, and perhaps should be removed or moved to the file toolkit instead.
[Description("Extract the contents of an archive.")]
[Input("archivePath", "The archive to extract.")]
[Input("targetDirectory", "The destination directory to extract into.")]
Expand Down
40 changes: 16 additions & 24 deletions Python_Engine/Compute/VirtualEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* along with this code. If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
*/

using BH.Engine.Base;
using BH.oM.Base.Attributes;
using BH.oM.Python;
using BH.oM.Python.Enums;
Expand All @@ -45,22 +46,17 @@ public static PythonEnvironment VirtualEnvironment(this PythonVersion version, s
BH.Engine.Base.Compute.RecordError("A BHoM Python virtual environment cannot cannot contain invalid filepath characters.");
return null;
}
if (version == PythonVersion.Undefined)
{
BH.Engine.Base.Compute.RecordError("Please provide a version of Python.");
return null;
}

// check that base environment is installed and return null and raise error if it isn't
string baseEnvironmentExecutable = Path.Combine(Query.DirectoryBaseEnvironment(), "python.exe");
if (!File.Exists(baseEnvironmentExecutable))
if (version == PythonVersion.Undefined)
{
BH.Engine.Base.Compute.RecordWarning("The base Python environment doesnt seem to be installed. Install it first in order to run this method.");
BH.Engine.Base.Compute.RecordError("Please provide a valid Python version.");
return null;
}

string targetExecutable = Query.VirtualEnvironmentExecutable(name);
string targetDirectory = Query.VirtualEnvironmentDirectory(name);
string versionExecutable = Path.Combine(Query.DirectoryBaseEnvironment(version), "python.exe");

bool exists = Query.VirtualEnvironmentExists(name);

if (exists && reload)
Expand All @@ -73,31 +69,28 @@ public static PythonEnvironment VirtualEnvironment(this PythonVersion version, s
RemoveKernel(name);
}

// download the target version of Python
string referencedExecutable = version.DownloadPython();

// move the directory containing referencedExecutable into Query.DirectoryBaseEnvironment() using the same name
string sourceDirectory = Path.GetDirectoryName(referencedExecutable);
string destinationDirectory = Path.Combine(Query.DirectoryBaseEnvironment(), new DirectoryInfo(Path.GetDirectoryName(referencedExecutable)).Name);
if (!Directory.Exists(destinationDirectory))
if (!File.Exists(versionExecutable))
{
Directory.Move(sourceDirectory, destinationDirectory);
// The output here should be the same, but to be sure replace the value.
BH.Engine.Base.Compute.RecordNote($"The base environment for the requested version {version} has been installed as it was not present.");
versionExecutable = BasePythonEnvironment(version, run: true)?.Executable;

if (versionExecutable == null) // If the executable is null, then python wasn't installed correctly, so return null - the error message for downloading the version should suffice.
return null;
}
if (Directory.Exists(sourceDirectory))
Directory.Delete(sourceDirectory, true);
referencedExecutable = Path.Combine(destinationDirectory, "python.exe");

// create the venv from the base environment
Process process = new Process()
{
StartInfo = new ProcessStartInfo()
{
FileName = Modify.AddQuotesIfRequired(baseEnvironmentExecutable),
Arguments = $"-m virtualenv --python={Modify.AddQuotesIfRequired(referencedExecutable)} {Modify.AddQuotesIfRequired(targetDirectory)}",
FileName = Modify.AddQuotesIfRequired(versionExecutable),
Arguments = $"-m virtualenv --python={Modify.AddQuotesIfRequired(versionExecutable)} {Modify.AddQuotesIfRequired(targetDirectory)}",
RedirectStandardError = true,
UseShellExecute = false,
}
};

using (Process p = Process.Start(process.StartInfo))
{
string standardError = p.StandardError.ReadToEnd();
Expand All @@ -120,15 +113,14 @@ public static PythonEnvironment VirtualEnvironment(this PythonVersion version, s
UseShellExecute = false,
}
};

using (Process p = Process.Start(process2.StartInfo))
{
string standardError = p.StandardError.ReadToEnd();
p.WaitForExit();
if (p.ExitCode != 0)
BH.Engine.Base.Compute.RecordError($"Error registering the \"{name}\" virtual environment.\n{standardError}");
}
// replace text in a file


return new PythonEnvironment() { Executable = targetExecutable, Name = name };
}
Expand Down
4 changes: 2 additions & 2 deletions Python_Engine/Convert/ToPython.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* This file is part of the Buildings and Habitats object Model (BHoM)
* Copyright (c) 2015 - 2024, the respective contributors. All rights reserved.
*
Expand Down Expand Up @@ -38,4 +38,4 @@ public static bool ToPython<T>(this T[,] input)
return false;
}
}
}
}
1 change: 0 additions & 1 deletion Python_Engine/Python/src/python_toolkit/__init__.py

This file was deleted.

23 changes: 23 additions & 0 deletions Python_Engine/Python/src/python_toolkit/bhom/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Root for the bhom subpackage."""

from pathlib import Path # pylint: disable=E0401
from os import path

from win32api import HIWORD, LOWORD, GetFileVersionInfo

BHOM_ASSEMBLIES_DIRECTORY = Path(path.expandvars("%PROGRAMDATA%/BHoM/Assemblies"))
BHOM_LOG_FOLDER = Path(path.expandvars("%PROGRAMDATA%/BHoM/Logs"))
TOOLKIT_NAME = "Python_Toolkit"

if not BHOM_LOG_FOLDER.exists():
BHOM_LOG_FOLDER = Path(path.expandvars("%TEMP%/BHoMLogs"))
BHOM_LOG_FOLDER.mkdir(exist_ok=True)

if not BHOM_ASSEMBLIES_DIRECTORY.exists():
BHOM_VERSION = ""
else:
_file_version_ms = GetFileVersionInfo(
(BHOM_ASSEMBLIES_DIRECTORY / "BHoM.dll").as_posix(), "\\"
)["FileVersionMS"]

BHOM_VERSION = f"{HIWORD(_file_version_ms)}.{LOWORD(_file_version_ms)}"
Loading