Skip to content

Commit 64c4e65

Browse files
committed
Merge branch 'wip/python-environment' into 'develop'
2 parents bb29e37 + 2a6cc90 commit 64c4e65

File tree

13 files changed

+320
-137
lines changed

13 files changed

+320
-137
lines changed

Automation/Scripts/automation_scripts/commands/distribution.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,19 +54,16 @@ def check_requirements(self, arguments: argparse.Namespace, **kwargs) -> None:
5454
def run(self, arguments: argparse.Namespace, simulate: bool, **kwargs) -> None:
5555
python_executable = sys.executable
5656
project_configuration: ProjectConfiguration = kwargs["configuration"]
57+
5758
all_python_packages = project_configuration.list_python_packages()
59+
python_package_metadata = project_configuration.get_python_package_metadata()
5860

5961
process_runner = ProcessRunner(ProcessSpawner(is_console = True))
6062
python_package_builder = PythonPackageBuilder(python_executable, process_runner)
6163

6264
logger.info("Generating python package metadata")
6365
for python_package in all_python_packages:
64-
python_package_builder.generate_package_metadata(
65-
product_identifier = project_configuration.project_identifier,
66-
project_version = project_configuration.project_version,
67-
copyright_text = project_configuration.copyright,
68-
python_package = python_package,
69-
simulate = simulate)
66+
python_package_builder.generate_package_metadata(python_package, python_package_metadata, simulate = simulate)
7067

7168

7269
async def run_async(self, arguments: argparse.Namespace, simulate: bool, **kwargs) -> None:
@@ -92,7 +89,6 @@ async def run_async(self, arguments: argparse.Namespace, simulate: bool, **kwarg
9289
python_executable = sys.executable
9390
project_configuration: ProjectConfiguration = kwargs["configuration"]
9491

95-
version = project_configuration.project_version.full_identifier
9692
all_python_packages = project_configuration.list_python_packages()
9793

9894
process_runner = ProcessRunner(ProcessSpawner(is_console = True))
@@ -104,7 +100,7 @@ async def run_async(self, arguments: argparse.Namespace, simulate: bool, **kwarg
104100
log_file_path = os.path.join("Artifacts", "Logs", "BuildDistributionPackage_%s.log" % python_package.identifier)
105101

106102
await python_package_builder.build_distribution_package(
107-
python_package, version, output_directory, log_file_path, simulate = simulate)
103+
python_package, output_directory, log_file_path, simulate = simulate)
108104

109105

110106
class _UploadCommand(AutomationCommand):

Automation/Scripts/automation_scripts/configuration/project_configuration.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from bhamon_development_toolkit.automation.project_version import ProjectVersion
55
from bhamon_development_toolkit.python.python_package import PythonPackage
6+
from bhamon_development_toolkit.python.python_package_metadata import PythonPackageMetadata
67

78

89
class ProjectConfiguration:
@@ -36,6 +37,15 @@ def get_artifact_default_parameters(self) -> dict:
3637
}
3738

3839

40+
def get_python_package_metadata(self) -> PythonPackageMetadata:
41+
return PythonPackageMetadata(
42+
product_identifier = self.project_identifier,
43+
version_identifier = self.project_version.full_identifier,
44+
revision_date = self.project_version.revision_date,
45+
copyright_text = self.copyright,
46+
)
47+
48+
3949
def list_automation_packages(self) -> List[PythonPackage]:
4050
return [
4151
PythonPackage(

Automation/Setup/python_helpers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def find_system_python_executable(python_versions: List[str]) -> Optional[str]:
4141
raise ValueError("Unsupported platform: '%s'" % platform.system())
4242

4343

44-
def setup_virtual_environment(python_system_executable: str, venv_directory: str, simulate: bool) -> None:
44+
def setup_virtual_environment(system_python_executable: str, venv_directory: str, simulate: bool) -> None:
4545
logger.info("Setting up python virtual environment (Path: %s)", venv_directory)
4646

4747
venv_python_executable = get_venv_python_executable(venv_directory)
@@ -54,7 +54,7 @@ def setup_virtual_environment(python_system_executable: str, venv_directory: str
5454
os.remove(os.path.join(venv_directory, "scripts", "python.exe"))
5555
shutil.rmtree(venv_directory)
5656

57-
run_python_command([ python_system_executable, "-m", "venv", venv_directory ], simulate = simulate)
57+
run_python_command([ system_python_executable, "-m", "venv", venv_directory ], simulate = simulate)
5858

5959
if platform.system() == "Darwin": # pylint: disable = no-else-raise
6060
raise NotImplementedError("MacOS is not supported")

Automation/Setup/setup.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,6 @@
1717

1818

1919
def main() -> None:
20-
21-
# Prevent active pyvenv from overriding a python executable specified in a command
22-
if "__PYVENV_LAUNCHER__" in os.environ:
23-
del os.environ["__PYVENV_LAUNCHER__"]
24-
2520
with automation_helpers.execute_in_workspace(__file__):
2621
arguments = parse_arguments()
2722
automation_helpers.configure_logging(arguments.verbosity)
@@ -31,11 +26,11 @@ def main() -> None:
3126

3227
logger.info("Setting up local workspace (Path: %s)", os.getcwd())
3328

34-
python_system_executable = python_helpers.find_and_check_system_python_executable(python_versions)
29+
system_python_executable = python_helpers.find_and_check_system_python_executable(python_versions)
3530
venv_python_executable = python_helpers.get_venv_python_executable(venv_directory)
3631
python_package_collection = [ "Automation/Scripts[dev]" ] + python_helpers.list_python_packages("Sources")
3732

38-
python_helpers.setup_virtual_environment(python_system_executable, venv_directory, simulate = arguments.simulate)
33+
python_helpers.setup_virtual_environment(system_python_executable, venv_directory, simulate = arguments.simulate)
3934
python_helpers.install_python_packages(venv_python_executable, python_package_collection, simulate = arguments.simulate)
4035

4136

Sources/toolkit/bhamon_development_toolkit/processes/process_helpers.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
import logging
12
import shlex
3+
import subprocess
24
from typing import List, Optional, TextIO
35

46
from bhamon_development_toolkit.logging.raw_logger import RawLogger
7+
from bhamon_development_toolkit.processes.exceptions.process_failure_exception import ProcessFailureException
8+
from bhamon_development_toolkit.processes.executable_command import ExecutableCommand
9+
from bhamon_development_toolkit.processes.process_result import ProcessResult
510

611

712
def format_executable_command(command: List[str]):
@@ -22,3 +27,40 @@ def create_raw_logger(stream: Optional[TextIO] = None, log_file_path: Optional[s
2227
raw_logger.configure_log_file(log_file_path, "debug", mode = "w", encoding = "utf-8")
2328

2429
return raw_logger
30+
31+
32+
def run_simple(logger: logging.Logger, command: ExecutableCommand,
33+
working_directory: Optional[str] = None, check_exit_code: bool = True, simulate: bool = False) -> ProcessResult:
34+
35+
logger.debug("+ %s", format_executable_command(command.get_command_for_logging()))
36+
37+
subprocess_options = {
38+
"cwd": working_directory,
39+
"capture_output": True,
40+
"text": True,
41+
"encoding": "utf-8",
42+
"stdin": subprocess.DEVNULL,
43+
}
44+
45+
if not simulate:
46+
result = subprocess.run(command.get_command(), check = False, **subprocess_options)
47+
for line in result.stdout.splitlines():
48+
logger.debug(line)
49+
for line in result.stderr.splitlines():
50+
logger.error(line)
51+
52+
if check_exit_code and result.returncode != 0:
53+
exception_message = "Subprocess failed (Executable: '%s', ExitCode: %s)" % (command.executable_path, result.returncode)
54+
raise ProcessFailureException(exception_message, command.executable_path, result.returncode)
55+
56+
return ProcessResult(
57+
executable = command.executable_path,
58+
exit_code = result.returncode,
59+
standard_output = result.stdout,
60+
error_output = result.stderr,
61+
)
62+
63+
return ProcessResult(
64+
executable = command.executable_path,
65+
exit_code = 0,
66+
)
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import logging
2+
import os
3+
import platform
4+
import shutil
5+
import sys
6+
from typing import List, Optional
7+
8+
from bhamon_development_toolkit.processes import process_helpers
9+
from bhamon_development_toolkit.processes.executable_command import ExecutableCommand
10+
11+
12+
logger = logging.getLogger("Python")
13+
14+
15+
class PythonEnvironment:
16+
17+
18+
def __init__(self, system_python_executable: str, venv_directory: str) -> None:
19+
self._system_python_executable = system_python_executable
20+
self._venv_directory = venv_directory
21+
22+
23+
def get_venv_directory(self) -> str:
24+
return self._venv_directory
25+
26+
27+
def get_venv_python_executable(self) -> str:
28+
return self.get_venv_executable("python")
29+
30+
31+
def get_venv_executable(self, executable: str) -> str:
32+
if platform.system() == "Windows":
33+
return os.path.join(self._venv_directory, "scripts", executable + ".exe")
34+
return os.path.join(self._venv_directory, "bin", executable)
35+
36+
37+
def _get_pip_configuration_file_path(self) -> str:
38+
if platform.system() == "Windows":
39+
return os.path.join(self._venv_directory, "pip.ini")
40+
return os.path.join(self._venv_directory, "pip.conf")
41+
42+
43+
def setup_virtual_environment(self, pip_configuration_file_path: Optional[str] = None, simulate: bool = False) -> None:
44+
venv_python_executable = self.get_venv_python_executable()
45+
if sys.executable.lower() == os.path.abspath(venv_python_executable).lower():
46+
raise RuntimeError("Active python is the target virtual environment")
47+
48+
if os.path.isdir(self._venv_directory) and not simulate:
49+
# Try to remove the executable first since it might be in use, otherwise we would be leaving a broken venv
50+
if platform.system() == "Windows" and os.path.exists(os.path.join(self._venv_directory, "scripts", "python.exe")):
51+
os.remove(os.path.join(self._venv_directory, "scripts", "python.exe"))
52+
shutil.rmtree(self._venv_directory)
53+
54+
venv_command = ExecutableCommand(self._system_python_executable)
55+
venv_command.add_arguments([ "-m", "venv", self._venv_directory ])
56+
57+
process_helpers.run_simple(logger, venv_command, simulate = simulate)
58+
59+
if pip_configuration_file_path is not None:
60+
pip_configuration_file_path_in_venv = self._get_pip_configuration_file_path()
61+
if not simulate:
62+
shutil.copy(pip_configuration_file_path, pip_configuration_file_path_in_venv)
63+
64+
self.install_python_packages([ "pip", "wheel" ], simulate = simulate)
65+
66+
67+
def install_python_packages(self,
68+
name_or_path_collection: List[str], simulate: bool = False) -> None:
69+
70+
install_command = ExecutableCommand(self.get_venv_python_executable())
71+
install_command.add_arguments([ "-m", "pip", "install", "--upgrade" ] + name_or_path_collection)
72+
73+
process_helpers.run_simple(logger, install_command, simulate = simulate)
74+
75+
76+
def install_python_packages_for_development(self,
77+
name_or_path_collection: List[str], simulate: bool = False) -> None:
78+
79+
def is_local_package(name_or_path: str) -> bool:
80+
return name_or_path.startswith(".") or "/" in name_or_path or "\\" in name_or_path
81+
82+
install_command = ExecutableCommand(self.get_venv_python_executable())
83+
install_command.add_arguments([ "-m", "pip", "install", "--upgrade" ])
84+
85+
for name_or_path in name_or_path_collection:
86+
install_command.add_arguments([ "--editable", name_or_path ] if is_local_package(name_or_path) else [ name_or_path ])
87+
88+
process_helpers.run_simple(logger, install_command, simulate = simulate)

Sources/toolkit/bhamon_development_toolkit/python/python_helpers.py

Lines changed: 18 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,19 @@
22
import os
33
import platform
44
import shutil
5-
import subprocess
65
import sys
76
from typing import List, Optional
87

9-
from bhamon_development_toolkit.processes import process_helpers
10-
from bhamon_development_toolkit.processes.executable_command import ExecutableCommand
11-
128

139
logger = logging.getLogger("Python")
1410

1511

12+
def resolve_system_python_executable() -> str:
13+
if hasattr(sys, "_base_executable"):
14+
return sys._base_executable # type: ignore # pylint: disable = protected-access
15+
raise RuntimeError("Unable to resolve the system Python executable")
16+
17+
1618
def find_and_check_system_python_executable(python_versions: List[str]) -> str:
1719
python_executable = find_system_python_executable(python_versions)
1820
if python_executable is None or not shutil.which(python_executable):
@@ -23,15 +25,11 @@ def find_and_check_system_python_executable(python_versions: List[str]) -> str:
2325

2426
def find_system_python_executable(python_versions: List[str]) -> Optional[str]:
2527
if platform.system() == "Linux":
26-
return "/usr/bin/python3"
27-
28-
if platform.system() == "Windows":
2928
possible_paths = []
3029

3130
for version in python_versions:
3231
possible_paths += [
33-
os.path.join(os.environ["SystemDrive"] + "\\", "Python%s" % version.replace(".", ""), "python.exe"),
34-
os.path.join(os.environ["ProgramFiles"], "Python%s" % version.replace(".", ""), "python.exe"),
32+
"/usr/bin/python" + version,
3533
]
3634

3735
for path in possible_paths:
@@ -40,74 +38,19 @@ def find_system_python_executable(python_versions: List[str]) -> Optional[str]:
4038

4139
return None
4240

43-
raise ValueError("Unsupported platform: '%s'" % platform.system())
44-
45-
46-
def setup_virtual_environment(python_system_executable: str, venv_directory: str, simulate: bool) -> None:
47-
logger.info("Setting up python virtual environment (Path: %s)", venv_directory)
48-
49-
venv_python_executable = get_venv_python_executable(venv_directory)
50-
if sys.executable.lower() == os.path.abspath(venv_python_executable).lower():
51-
raise RuntimeError("Active python is the target virtual environment")
52-
53-
if os.path.isdir(venv_directory) and not simulate:
54-
# Try to remove the executable first since it might be in use, otherwise we would be leaving a broken venv
55-
if platform.system() == "Windows" and os.path.exists(os.path.join(venv_directory, "scripts", "python.exe")):
56-
os.remove(os.path.join(venv_directory, "scripts", "python.exe"))
57-
shutil.rmtree(venv_directory)
58-
59-
venv_command = ExecutableCommand(python_system_executable)
60-
venv_command.add_arguments([ "-m", "venv", venv_directory ])
61-
62-
run_python_command(venv_command, simulate = simulate)
63-
64-
if platform.system() in [ "Darwin", "Linux" ] and not os.path.exists(os.path.join(venv_directory, "scripts")) and not simulate:
65-
os.symlink("bin", os.path.join(venv_directory, "scripts"))
66-
67-
install_python_packages(venv_python_executable, [ "pip", "wheel" ], simulate = simulate)
68-
69-
70-
def get_venv_python_executable(venv_directory: str) -> str:
7141
if platform.system() == "Windows":
72-
return os.path.join(venv_directory, "scripts", "python.exe")
73-
return os.path.join(venv_directory, "bin", "python")
74-
75-
76-
def install_python_packages(python_executable: str,
77-
name_or_path_collection: List[str], python_package_repository: Optional[str] = None, simulate: bool = False) -> None:
78-
79-
def is_local_package(name_or_path: str) -> bool:
80-
return name_or_path.startswith(".") or "/" in name_or_path or "\\" in name_or_path
81-
82-
install_command = ExecutableCommand(python_executable)
83-
install_command.add_arguments([ "-m", "pip", "install", "--upgrade" ])
84-
85-
if python_package_repository is not None:
86-
install_command.add_arguments([ "--extra-index", python_package_repository ])
87-
88-
for name_or_path in name_or_path_collection:
89-
install_command.add_arguments([ "--editable", name_or_path ] if is_local_package(name_or_path) else [ name_or_path ])
90-
91-
run_python_command(install_command, simulate = simulate)
92-
42+
possible_paths = []
9343

94-
def run_python_command(command: ExecutableCommand, working_directory: Optional[str] = None, simulate: bool = False) -> None:
95-
logger.info("+ %s", process_helpers.format_executable_command(command.get_command_for_logging()))
44+
for version in python_versions:
45+
possible_paths += [
46+
os.path.join(os.environ["SystemDrive"] + "\\", "Python%s" % version.replace(".", ""), "python.exe"),
47+
os.path.join(os.environ["ProgramFiles"], "Python%s" % version.replace(".", ""), "python.exe"),
48+
]
9649

97-
subprocess_options = {
98-
"cwd": working_directory,
99-
"capture_output": True,
100-
"text": True,
101-
"encoding": "utf-8",
102-
"stdin": subprocess.DEVNULL,
103-
}
50+
for path in possible_paths:
51+
if os.path.exists(path):
52+
return path
10453

105-
if not simulate:
106-
process_result = subprocess.run(command.get_command(), check = False, **subprocess_options)
107-
for line in process_result.stdout.splitlines():
108-
logger.debug(line)
109-
for line in process_result.stderr.splitlines():
110-
logger.error(line)
54+
return None
11155

112-
if process_result.returncode != 0:
113-
raise RuntimeError("Python command failed (ExitCode: %r)" % process_result.returncode)
56+
raise ValueError("Unsupported platform: '%s'" % platform.system())

0 commit comments

Comments
 (0)