Skip to content

Commit 618dfb0

Browse files
Refactor to use the windows installer when creating a base environment (#145)
2 parents 21c3ce8 + 37eb521 commit 618dfb0

File tree

21 files changed

+491
-182
lines changed

21 files changed

+491
-182
lines changed

Python_Engine/Compute/BasePythonEnvironment.cs

Lines changed: 11 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,14 @@ namespace BH.Engine.Python
3333
{
3434
public static partial class Compute
3535
{
36+
[PreviousVersion("8.0", "BH.Engine.Python.Compute.BasePythonEnvironment(System.Boolean, System.Boolean)")]
3637
[Description("Retrieve or reinstall the base Python Environment for BHoM workflows.")]
38+
[Input("version", "The target version of python to be installed or retrieved.")]
3739
[Input("reload", "Reload the base Python environment rather than recreating it, if it already exists.")]
3840
[Input("run", "Start the installation/retrieval of the BHoM Base Python Environment.")]
3941
[Output("env", "The base Python Environment for all BHoM workflows.")]
4042
public static PythonEnvironment BasePythonEnvironment(
43+
PythonVersion version = PythonVersion.v3_10,
4144
bool reload = true,
4245
bool run = false
4346
)
@@ -52,10 +55,10 @@ public static PythonEnvironment BasePythonEnvironment(
5255
// create PythonEnvironments directory if it doesnt already exist
5356
Directory.CreateDirectory(Query.DirectoryEnvironments());
5457
}
55-
58+
5659
// determine whether the base environment already exists
57-
string targetExecutable = Path.Combine(Query.DirectoryBaseEnvironment(), "python.exe");
58-
bool exists = Directory.Exists(Query.DirectoryBaseEnvironment()) && File.Exists(targetExecutable);
60+
string targetExecutable = Path.Combine(Query.DirectoryBaseEnvironment(version), "python.exe");
61+
bool exists = File.Exists(targetExecutable);
5962

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

67-
// download the target Python version and convert into a "full" python installation bypassing admin rights
68-
string executable = PythonVersion.v3_10_5.DownloadPython(Query.ToolkitName());
69-
string pipInstaller = DownloadGetPip(Path.GetDirectoryName(executable));
70-
string baseEnvironmentDirectory = Path.GetDirectoryName(executable);
71-
72-
// install pip into the python installation
73-
Process process = new Process()
74-
{
75-
StartInfo = new ProcessStartInfo()
76-
{
77-
FileName = Modify.AddQuotesIfRequired(executable),
78-
Arguments = Modify.AddQuotesIfRequired(pipInstaller) + " --no-warn-script-location",
79-
RedirectStandardError=true,
80-
UseShellExecute=false,
81-
}
82-
};
83-
using (Process p = Process.Start(process.StartInfo))
84-
{
85-
string standardError = p.StandardError.ReadToEnd();
86-
p.WaitForExit();
87-
if (p.ExitCode != 0)
88-
BH.Engine.Base.Compute.RecordError($"Error installing pip.\n{standardError}");
89-
File.Delete(pipInstaller);
90-
}
91-
92-
// delete files with the suffix ._pth from installedDirectory
93-
List<string> pthFiles = Directory.GetFiles(baseEnvironmentDirectory, "*.*", SearchOption.TopDirectoryOnly).Where(s => s.EndsWith("._pth")).ToList();
94-
foreach (string pthFile in pthFiles)
95-
{
96-
File.Delete(pthFile);
97-
}
98-
99-
// move files with the suffix .dll and .pyd from installedDirectory into a DLLs directory
100-
string libDirectory = Directory.CreateDirectory(Path.Combine(baseEnvironmentDirectory, "DLLs")).FullName;
101-
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();
102-
foreach (string libFile in libFiles)
103-
{
104-
File.Move(libFile, Path.Combine(libDirectory, Path.GetFileName(libFile)));
105-
}
70+
// download and run the installer for the target Python version
71+
string exe = version.DownloadPythonVersion();
10672

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

110-
return new PythonEnvironment() { Name = Query.ToolkitName(), Executable = executable };
77+
return new PythonEnvironment() { Name = Query.ToolkitName(), Executable = exe };
11178
}
11279
}
11380
}

Python_Engine/Compute/Download.cs

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
using BH.oM.Python.Enums;
2525
using System;
2626
using System.ComponentModel;
27+
using System.Diagnostics;
2728
using System.IO;
29+
using System.Reflection;
2830
using System.Xml.Linq;
2931

3032
namespace BH.Engine.Python
@@ -85,34 +87,53 @@ public static string DownloadFile(
8587
return filePath;
8688
}
8789

88-
// TODO - THIS METHOD HAS CHANGED BUT IS STILL USED, SO NEEDS DEPRECATING
89-
// changed from what to what ?
90-
[Description("Download the target version of Python.")]
91-
[Input("version", "A Python version.")]
92-
[Input("name", "Name of target exe file.")]
93-
[Output("executablePath", "The path of the executable for the downloaded Python.")]
94-
public static string DownloadPython(this PythonVersion version, string name = null)
90+
/******************************************************/
91+
92+
[PreviousVersion("8.0", "BH.Engine.Python.Compute.DownloadPython(BH.oM.Python.Enums.PythonVersion, System.String)")]
93+
[Description("Download and install a specified version of python, and return the executable for it.")]
94+
[Input("version", "The version of python to download.")]
95+
[Output("pythonExecutable", "The executable (python.exe) for the python version that was installed")]
96+
public static string DownloadPythonVersion(this PythonVersion version)
9597
{
9698
string url = version.EmbeddableURL();
97-
if (string.IsNullOrEmpty(name))
98-
name = Path.GetFileNameWithoutExtension(url);
99-
string targetExecutable = Path.Combine(Query.DirectoryEnvironments(), name, "python.exe");
100-
101-
if (File.Exists(targetExecutable))
102-
return targetExecutable;
103-
104-
string zipfile = DownloadFile(url, Query.DirectoryEnvironments());
105-
UnzipFile(zipfile, Query.DirectoryEnvironments(), name, true);
106-
107-
return targetExecutable;
108-
}
10999

110-
[Description("Download the pip installer")]
111-
[Input("targetDirectory", "The directory into which get-pip.py will be downloaded.")]
112-
[Output("getpipPath", "The path of the file used to install pip into an embedded Python environment.")]
113-
public static string DownloadGetPip(string targetDirectory)
114-
{
115-
return DownloadFile("https://bootstrap.pypa.io/get-pip.py", targetDirectory);
100+
string basePath = Path.Combine(Query.DirectoryBaseEnvironment(version));
101+
102+
if (File.Exists(Path.Combine(basePath, "python.exe")))
103+
return Path.Combine(basePath, "python.exe");
104+
105+
if (!Directory.Exists(basePath))
106+
Directory.CreateDirectory(basePath);
107+
else
108+
{
109+
Directory.Delete(basePath, true); //if there are any files here already for some reason, remove them.
110+
Directory.CreateDirectory(basePath);
111+
}
112+
113+
string installerFile = DownloadFile(url, basePath, "installer.exe");
114+
115+
using (Process install = new Process()
116+
{
117+
StartInfo = new ProcessStartInfo()
118+
{
119+
FileName = installerFile,
120+
Arguments = $"/passive InstallAllUsers=0 InstallLauncherAllUsers=0 Include_launcher=0 Shortcuts=0 AssociateFiles=0 Include_tools=0 Include_test=0 TargetDir={Modify.AddQuotesIfRequired(basePath)}",
121+
RedirectStandardError = true,
122+
UseShellExecute = false,
123+
}
124+
})
125+
{
126+
install.Start();
127+
string stderr = install.StandardError.ReadToEnd();
128+
install.WaitForExit();
129+
if (install.ExitCode != 0)
130+
{
131+
BH.Engine.Base.Compute.RecordError($"Error installing python: {stderr}");
132+
return null;
133+
}
134+
}
135+
136+
return Path.Combine(basePath, "python.exe");
116137
}
117138
}
118139
}

Python_Engine/Compute/Remove.cs

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@
2121
*/
2222

2323
using BH.oM.Base.Attributes;
24+
using BH.oM.Python.Enums;
25+
using System;
26+
using System.Collections.Generic;
2427
using System.ComponentModel;
28+
using System.Diagnostics;
2529
using System.IO;
2630

2731
namespace BH.Engine.Python
@@ -77,19 +81,60 @@ public static void RemoveAllVirtualEnvironments()
7781
}
7882
}
7983

80-
[Description("Completely remove the base BHoM Python environment.")]
81-
public static void RemoveBaseEnvironment()
84+
[PreviousVersion("8.0", "BH.Engine.Python.Compute.RemoveBaseEnvironment()")]
85+
[Description("Remove the base install for the python version specified.")]
86+
[Input("version", "The base python version to remove.")]
87+
public static void RemoveBaseVersion(PythonVersion version = PythonVersion.v3_10)
8288
{
83-
string basePath = Path.Combine(Query.DirectoryEnvironments(), Query.ToolkitName());
84-
if (Directory.Exists(basePath))
89+
string basePath = Path.Combine(Query.DirectoryEnvironments(), Query.ToolkitName(), version.ToString());
90+
string baseInstaller = Path.Combine(basePath, "installer.exe");
91+
92+
// If the installer does not exist and the folder does - assume a bad/invalid install and just delete the entire folder.
93+
if (!File.Exists(baseInstaller) && Directory.Exists(basePath))
94+
{
8595
Directory.Delete(basePath, true);
96+
return;
97+
}
98+
else if (!Directory.Exists(basePath))
99+
// If the base path doesn't exist there is nothing to remove.
100+
return;
101+
102+
using (Process uninstall = new Process()
103+
{
104+
StartInfo = new ProcessStartInfo()
105+
{
106+
FileName = baseInstaller,
107+
Arguments = "/uninstall /passive",
108+
RedirectStandardError = true,
109+
UseShellExecute = false,
110+
}
111+
})
112+
{
113+
uninstall.Start();
114+
string stderr = uninstall.StandardError.ReadToEnd();
115+
uninstall.WaitForExit();
116+
if (uninstall.ExitCode != 0)
117+
{
118+
BH.Engine.Base.Compute.RecordError($"Error uninstalling python: {stderr}");
119+
return;
120+
}
121+
}
122+
123+
// Finally remove base folder as the installer may have missed something.
124+
Directory.Delete(basePath, true);
86125
}
87126

88127
[Description("Completely remove all BHoM-related Python environments and kernels.")]
89128
public static void RemoveEverything()
90129
{
91130
RemoveAllVirtualEnvironments();
92-
RemoveBaseEnvironment();
131+
132+
//remove all python versions installed in base directory.
133+
foreach (PythonVersion e in Enum.GetValues(typeof(PythonVersion)))
134+
{
135+
if (e != PythonVersion.Undefined)
136+
RemoveBaseVersion(e);
137+
}
93138
}
94139
}
95140
}

Python_Engine/Compute/UnzipFile.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ namespace BH.Engine.Python
2929
{
3030
public static partial class Compute
3131
{
32+
//This method is no longer used by python toolkit, and perhaps should be removed or moved to the file toolkit instead.
3233
[Description("Extract the contents of an archive.")]
3334
[Input("archivePath", "The archive to extract.")]
3435
[Input("targetDirectory", "The destination directory to extract into.")]

Python_Engine/Compute/VirtualEnvironment.cs

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
* along with this code. If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
2121
*/
2222

23+
using BH.Engine.Base;
2324
using BH.oM.Base.Attributes;
2425
using BH.oM.Python;
2526
using BH.oM.Python.Enums;
@@ -45,22 +46,17 @@ public static PythonEnvironment VirtualEnvironment(this PythonVersion version, s
4546
BH.Engine.Base.Compute.RecordError("A BHoM Python virtual environment cannot cannot contain invalid filepath characters.");
4647
return null;
4748
}
48-
if (version == PythonVersion.Undefined)
49-
{
50-
BH.Engine.Base.Compute.RecordError("Please provide a version of Python.");
51-
return null;
52-
}
5349

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

6256
string targetExecutable = Query.VirtualEnvironmentExecutable(name);
6357
string targetDirectory = Query.VirtualEnvironmentDirectory(name);
58+
string versionExecutable = Path.Combine(Query.DirectoryBaseEnvironment(version), "python.exe");
59+
6460
bool exists = Query.VirtualEnvironmentExists(name);
6561

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

76-
// download the target version of Python
77-
string referencedExecutable = version.DownloadPython();
78-
79-
// move the directory containing referencedExecutable into Query.DirectoryBaseEnvironment() using the same name
80-
string sourceDirectory = Path.GetDirectoryName(referencedExecutable);
81-
string destinationDirectory = Path.Combine(Query.DirectoryBaseEnvironment(), new DirectoryInfo(Path.GetDirectoryName(referencedExecutable)).Name);
82-
if (!Directory.Exists(destinationDirectory))
72+
if (!File.Exists(versionExecutable))
8373
{
84-
Directory.Move(sourceDirectory, destinationDirectory);
74+
// The output here should be the same, but to be sure replace the value.
75+
BH.Engine.Base.Compute.RecordNote($"The base environment for the requested version {version} has been installed as it was not present.");
76+
versionExecutable = BasePythonEnvironment(version, run: true)?.Executable;
77+
78+
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.
79+
return null;
8580
}
86-
if (Directory.Exists(sourceDirectory))
87-
Directory.Delete(sourceDirectory, true);
88-
referencedExecutable = Path.Combine(destinationDirectory, "python.exe");
8981

9082
// create the venv from the base environment
9183
Process process = new Process()
9284
{
9385
StartInfo = new ProcessStartInfo()
9486
{
95-
FileName = Modify.AddQuotesIfRequired(baseEnvironmentExecutable),
96-
Arguments = $"-m virtualenv --python={Modify.AddQuotesIfRequired(referencedExecutable)} {Modify.AddQuotesIfRequired(targetDirectory)}",
87+
FileName = Modify.AddQuotesIfRequired(versionExecutable),
88+
Arguments = $"-m virtualenv --python={Modify.AddQuotesIfRequired(versionExecutable)} {Modify.AddQuotesIfRequired(targetDirectory)}",
9789
RedirectStandardError = true,
9890
UseShellExecute = false,
9991
}
10092
};
93+
10194
using (Process p = Process.Start(process.StartInfo))
10295
{
10396
string standardError = p.StandardError.ReadToEnd();
@@ -120,15 +113,14 @@ public static PythonEnvironment VirtualEnvironment(this PythonVersion version, s
120113
UseShellExecute = false,
121114
}
122115
};
116+
123117
using (Process p = Process.Start(process2.StartInfo))
124118
{
125119
string standardError = p.StandardError.ReadToEnd();
126120
p.WaitForExit();
127121
if (p.ExitCode != 0)
128122
BH.Engine.Base.Compute.RecordError($"Error registering the \"{name}\" virtual environment.\n{standardError}");
129123
}
130-
// replace text in a file
131-
132124

133125
return new PythonEnvironment() { Executable = targetExecutable, Name = name };
134126
}

Python_Engine/Convert/ToPython.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/*
22
* This file is part of the Buildings and Habitats object Model (BHoM)
33
* Copyright (c) 2015 - 2024, the respective contributors. All rights reserved.
44
*
@@ -38,4 +38,4 @@ public static bool ToPython<T>(this T[,] input)
3838
return false;
3939
}
4040
}
41-
}
41+
}

Python_Engine/Python/src/python_toolkit/__init__.py

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""Root for the bhom subpackage."""
2+
3+
from pathlib import Path # pylint: disable=E0401
4+
from os import path
5+
6+
from win32api import HIWORD, LOWORD, GetFileVersionInfo
7+
8+
BHOM_ASSEMBLIES_DIRECTORY = Path(path.expandvars("%PROGRAMDATA%/BHoM/Assemblies"))
9+
BHOM_LOG_FOLDER = Path(path.expandvars("%PROGRAMDATA%/BHoM/Logs"))
10+
TOOLKIT_NAME = "Python_Toolkit"
11+
12+
if not BHOM_LOG_FOLDER.exists():
13+
BHOM_LOG_FOLDER = Path(path.expandvars("%TEMP%/BHoMLogs"))
14+
BHOM_LOG_FOLDER.mkdir(exist_ok=True)
15+
16+
if not BHOM_ASSEMBLIES_DIRECTORY.exists():
17+
BHOM_VERSION = ""
18+
else:
19+
_file_version_ms = GetFileVersionInfo(
20+
(BHOM_ASSEMBLIES_DIRECTORY / "BHoM.dll").as_posix(), "\\"
21+
)["FileVersionMS"]
22+
23+
BHOM_VERSION = f"{HIWORD(_file_version_ms)}.{LOWORD(_file_version_ms)}"

0 commit comments

Comments
 (0)