Newer
Older
"""mpsd-software-environment: tool for installation of toolchains."""
import argparse
import datetime
import logging
from typing import List, Tuple, Union
# If 'rich' is available ("pip install rich" or "apt-get install python3-rich"),
# then use coloured output, otherwise proceed as before
try:
import rich.logging
except ModuleNotFoundError:
rich_available = False
else:
rich_available = True
This function builds toolchains for MPSD-HPC at the appropriate directory, \n
for given system architecture and MPSD software stack version.\n
The toolchains
are built using the bash script spack_setup.sh, and the results are logged. """
call_date_iso = (
datetime.datetime.now().replace(microsecond=0).isoformat().replace(":", "-")
)
"cmd_log_file": "script_execution_summary.log",
"metadata_tag_open": "!<meta>",
"metadata_tag_close": "</meta>!",
"spack_environments_repo": "https://gitlab.gwdg.de/mpsd-cs/spack-environments.git",
}
def create_log_file_names(
mpsd_release: str,
mpsd_microarch: str,
action: str,
date: str = call_date_iso,
toolchain: str = None,
) -> Union[str, None]:
This function creates the log file names for either the installer or
If a toolchain is given, then the build log file name is created.
if no toolchain is given, then the installer log file name is created.
The installer log file hosts the logs of the installer script, while
the build log file hosts the logs of the build process as generated by the
spack_setup.sh script.
Parameters
----------
mpsd_release : str
MPSD software stack version
mpsd_microarch : str
system architecture
date : str
date of the call ins iso format
action : str
action performed (install,remove,reinstall,prepare,status)
only install and remove are valid for build log file.
toolchain : str
toolchain name (only for build log file)
log file name
installer_log_file_name or build_log_file_name depending on the
parameters given.
If the action is not one that changes the files on disk ( info only actions)
then None is returned.
if toolchain:
# if toolchain is given, then we build the build_log_file_name
if action in ["install", "remove"]:
log_file_name = (
f"{mpsd_release}_{mpsd_microarch}_{date}_BUILD_{toolchain}_{action}.log"
)
else:
# if toolchain is not given, then we build the installer_log_file_name
log_file_name = f"{mpsd_release}_{mpsd_microarch}_{date}_APEX_{action}.log"
return log_file_name
def log_metadata(key: str, value: str) -> None:
"""Log metadata to the log file.
This function logs metadata to the log file. The metadata is
enclosed in a tag, so that it can be easily found in the log file.
logging module is used to write the metadata to the log file.
Parameters
----------
key : str
key of the metadata
value : str
value of the metadata
returns : None
"""
logging.info(
f"{config_vars['metadata_tag_open']}{key}:{value}{config_vars['metadata_tag_close']}"
)
def read_metadata_from_logfile(logfile: Union[str, Path]) -> dict:
"""Read metadata from the log file.
This function reads metadata from the log file. The metadata is
enclosed in a tag, so that it can be easily found in the log file.
Parameters
----------
logfile : str or Path
log file name
returns : dict
dictionary containing the metadata
"""
with open(logfile, "r") as f:
log_text = f.read()
# check for all data that matches the regex
# metadata_tag_open {key}:{value} metadata_tag_close
# and return a dictionary with all the matches
return {
match.group(1): match.group(2)
for match in re.finditer(
f"{config_vars['metadata_tag_open']}(\w+):(\w+){config_vars['metadata_tag_close']}",
log_text,
)
}
def get_installer_log_file_path(mpsd_release: str, cmd: str, script_dir: str) -> str:

Ashwin Kumar Karnad
committed
# Get machine configs
os.environ.get("MPSD_OS", "UNKNOWN_OS")
mpsd_microarch = get_native_microarchitecture()
# parse logging first
# decide the log_file_name
installer_log_name = create_log_file_names(
mpsd_release=mpsd_release, mpsd_microarch=mpsd_microarch, action=cmd

Ashwin Kumar Karnad
committed
)
log_folder = script_dir / mpsd_release / "logs"

Ashwin Kumar Karnad
committed
# if the log_folder dosent exist, dont log this message if
# the command is a info-only command
if cmd not in ["status", "available"]:

Ashwin Kumar Karnad
committed
if not os.path.exists(log_folder):
os.makedirs(log_folder)
installer_log_file = log_folder / installer_log_name
else:
installer_log_file = None
return installer_log_file
def set_up_logging(loglevel="warning", file_path=None):
"""Set up logging.
This function sets up the logging configuration for the script.
Parameters
----------
loglevel : str or int
Loglevels are:
- warning (default): only print statements if something is unexpected
- info (show more detailed progress)
- debug (show very detailed output)
- filename to save logging messages into
If loglevel is 'debug', save line numbers in log messages.
Returns
-------
None.
Logger instances are generally not passed around, but retrieved from the
logging module as shown below (they are singletons).
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
We provide two loggers:
1. log = logging.getLogger('')
This is the 'root' logger. It uses a RichHandler if rich is available for
output to the shell, otherwise plain text.
Typical use:
log.debug("...")
log.info("...")
log.warn("...")
Equivalent to
logging.debug("...")
logging.info("...")
2. print_log = logging.getlogger('print')
This uses the logging module to issue the message, but prints without
any further markup (i.e. no date, loglevel, line number, etc). Think
PRINT via the LOGging module.
We use this as a replacement for the print function (i.e. for messages
that should not be affected by logging levels, and which should always
be printed).
Typical and intended use:
print_log.info("Available toolchains are ...")
The major difference from the normal print command is that the output
will be send to the stdout (as for print) AND the file with name
filename, so that these messages appear in the log file together with
normal log output.
# convert loglevel string into loglevel as number
log_level_numeric = getattr(logging, loglevel.upper(), logging.WARNING)
if not isinstance(log_level_numeric, int):
# set up the main logger ("root" logger)
logger = logging.getLogger("")
# - "logger" logs everything
# - we use loglevel at handler level to write everything to file
# - and filter using log_level_numeric (as the user provides) to
# send logging messages to the console
logger.setLevel(0)
# the handler determines where the logs go: stdout/file
if rich_available:
# https://rich.readthedocs.io/en/stable/logging.html
shell_handler = rich.logging.RichHandler()
# rich handler provides metadata automatically:
logging_format = "%(message)s"
# for shell output, only show time (not date and time)
shell_formatter = logging.Formatter(logging_format, datefmt="[%X]")
else:
shell_handler = logging.StreamHandler()
# include line numbers in output if level is DEBUG
linenumbers = " %(lineno)4d" if log_level_numeric == logging.DEBUG else ""
logging_format = "%(asctime)s %(levelname)7s" + linenumbers + " | %(message)s"
shell_formatter = logging.Formatter(logging_format)
# here we hook everything together
shell_handler.setFormatter(shell_formatter)
# use the log_level_numeric to decide how much logging is sent to shell
shell_handler.setLevel(log_level_numeric)
# Here we set the handlers of the RootLogger to be just the one we want.
# The reason is that the logging module will add a <StreamHandler <stderr>
# (NOTSET)> handler if logging.info/logging.debug/... is used before we
# come across this line. And we do not want that additional handler.
logger.handlers = [shell_handler]
# if filename provided, write log messages to that file, too.
if file_path:
file_handler = logging.FileHandler(file_path)
# if we have a file, we write all information in there.
# We could change the level, for example restrict to only DEBUG and above with
# file_handler.setLevel(logging.DEBUG)
file_logging_format = "%(asctime)s %(levelname)7s %(lineno)4d | %(message)s"
file_formatter = logging.Formatter(file_logging_format, datefmt="[%X]")
file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler)
#
# new logger for printing
#
print_log = logging.getLogger("print")
print_log.setLevel(logging.INFO)
print_log.propagate = False
# create formatter 'empty' formatter
formatter = logging.Formatter("%(message)s")
ch = logging.StreamHandler()
ch.setFormatter(formatter)
print_log.addHandler(ch)
# if filename provided, write output of print_log to that file, too
# create, format and add file handler
fh = logging.FileHandler(file_path)
fh.setFormatter(formatter)
print_log.addHandler(fh)
#
# short message
#
f"Logging has been setup, loglevel={loglevel.upper()} "
+ f"{file_path=} {rich_available=}"
# Helper class to change directory via context manager
class os_chdir:
"""The os_chdir class is a context manager.
It changes the current directory to a specified directory
and returns to the original directory after execution.
"""
"""Initialize, save original directory."""
self.new_dir = new_dir
self.saved_dir = os.getcwd()
"""Go to target directory (main action for context)."""
def __exit__(self, exc_type, exc_val, exc_tb):
"""On exist we return to original directory."""
def run(*args, counter=[0], **kwargs):
"""
Convenience function to call `subprocess.run` and provide some metadata
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
about the call.
Parameters
----------
args : tuple
passed on to subprocess.run(*args). For example
("ls -l") or (["ls", "-l"])
counter : TYPE, optional
list with one integer, starting from [0].
This is (a Python hack) to count the number of
calls of this function, so the different calls of subprocess.run
are easier to follow in the log files.
kwargs : dict
keyword-value arguments to be passed to subprocess.run. For example,
`shell=True`.
Returns
-------
process : subprocess.CompletedProcess
CompletedProcess object as returned by `subprocess.run` .
Examples
--------
>>> run(['date', '+%Y-%m-%d'])
##-03 Starting subprocess.run(['date', '+%Y-%m-%d']) with options
##-03 getcwd=/Users/fangohr/git/mpsd-software-environments
##-03 COMMAND=date +%Y-%m-%d
2023-05-30
##-03 Completed in 0.0054s.
##-03
CompletedProcess(args=['date', '+%Y-%m-%d'], returncode=0)
>>> run(['date +%Y-%m-%d'], shell=True)
##-04 Starting subprocess.run(['date +%Y-%m-%d']) with options shell=True
##-04 getcwd=/Users/fangohr/git/mpsd-software-environments
##-04 COMMAND=date +%Y-%m-%d
2023-05-30
##-04 Completed in 0.0069s.
##-04
CompletedProcess(args=['date +%Y-%m-%d'], returncode=0)
"""
# token is printed in front of every meta-data line - useful for
# searching the logs. Starts with "##-00", then "##-01", ...
token = f"##-{counter[0]:02d}"
counter[0] += 1 # increase counter
# make command nicely readable: ["ls", "-l"] -> "ls -l"
assert isinstance(args, tuple)
assert len(args) == 1
arg = args[0]
# either args is a tuple containing a string | Example: ('ls -1',)
if isinstance(arg, str):
command = arg
# or we have a tuple containing a list of strings.
# Example: (['ls', '-1'],)
elif isinstance(arg, list):
command = " ".join(arg)
else:
# we do not expect this to happen
raise NotImplementedError(f"{arg=}, {args=}")
# make options (such as `shell=True`) nicely readable
options = ", ".join([f"{key}={value}" for key, value in kwargs.items()])
# provide information about upcoming subprocess.run call
logging.info(f"{token} Starting subprocess.run('{command}') with options {options}")
logging.debug(f"{token} getcwd={os.getcwd()}")
logging.debug(f"{token} exact call: subprocess.run({arg})")
time_start = time.time()
process = subprocess.run(*args, **kwargs)
execution_time = time.time() - time_start
logging.debug(f"{token} Completed in {execution_time:.4f}s.")
logging.debug(f"{token}") # near-empty line to make reading logs easier
mpsd_release: str, script_dir: str, msg: str = None, **kwargs
Log the command used to build the toolchains.
It also logs information about the software environment installer branch,
the Spack environments branch, and the commit hashes of each.
It also logs steps taken
in the install process using the optional message argument.
Parameters
----------
- mpsd_release : str
The name of the release to install toolchains for.
- script_dir : str
The path to the directory where the scripts are located.
- msg : str, optional
An optional message to log in the command log file.
The name of the Spack environments branch.
- spe_commit_hash : str
The commit hash of the Spack environments branch.
Returns
-------
- None
release_base_dir = script_dir / mpsd_release
# Write to the log file with the following format
# --------------------------------------------------
# 2023-02-29T23:32:01, install-software-environment.py --release 23b --install ALL
# Software environment installer branch: script_branch (commit hash: \
# script_commit_hash)
# Spack environments branch: dev-23a (commit hash: spe_commit_hash)
# MSGs
with os_chdir(release_base_dir):
with open(config_vars["cmd_log_file"], "a") as f:
if msg:
# Write the message to the log file
f.write(msg + "\n")
else:
# Write the header
f.write("-" * 50 + "\n")
# Gather data to log
# call statement:
cmd_line = " ".join(sys.argv)
# script branch and commit hash
with os_chdir(script_dir):
script_branch = (
["git", "rev-parse", "--abbrev-ref", "HEAD"],
stdout=subprocess.PIPE,
.stdout.decode()
.strip()
)
script_commit_hash = (
["git", "rev-parse", "--short", "HEAD"],
stdout=subprocess.PIPE,
.stdout.decode()
.strip()
# spack-environments branch and commit hash from kwargs
spe_branch = kwargs.get("spe_branch", None)
spe_commit_hash = kwargs.get("spe_commit_hash", None)
# Write to log file
f.write(f"{datetime.datetime.now().isoformat()}, {cmd_line}\n")
f"Software environment installer branch: {script_branch} "
f"(commit hash: {script_commit_hash})\n"
f"Spack environments branch: {spe_branch} "
f"(commit hash: {spe_commit_hash})\n"
def create_dir_structure(mpsd_release: str, script_dir: Path) -> None:
"""
Create the directory structure and clone spack environments repo.
The create_dir_structure function creates the directory structure for
the specified release and clones the Spack environments repository if it
doesn't exist.
- mpsd_release: A string representing the MPSD release version.
- script_dir: A Path object representing the path to the scripts directory.
Returns
-------
# Create the directory structure for the release
release_base_dir = script_dir / mpsd_release
release_base_dir.mkdir(parents=True, exist_ok=True)
with os_chdir(release_base_dir):
# Clone the spack-environments repo if it doesn't exist
if not os.path.exists("spack-environments"):
"clone",
config_vars["spack_environments_repo"],
with os_chdir("spack-environments"):
# Git fetch and checkout the release branch and git pull
# to be sure that the resulting repo is up to date
run(["git", "fetch", "--all"], check=True)
checkout_result = run(["git", "checkout", mpsd_release], check=True)
if checkout_result.returncode != 0:
"Release branch does not exist in spack-environment repo \n."
"Check for typos."
run(["git", "pull"], check=True)
def get_release_info(mpsd_release: str, script_dir: Path) -> Tuple[str, str, List[str]]:
Get information about the specified release.
Get information about the specified release, such as the branch and commit hash
of the Spack environments repository and the available toolchains.
Parameters
----------
The name of the release to get information for.
The base directory where releases are stored.
Returns
-------
The name of the branch for the Spack environments repository.
The commit hash for the Spack environments repository.
available_toolchains : list
A list of strings representing the available toolchains for the release.
Raises
------
FileNotFoundError
If the release directory does not exist. Run `create_dir_structure()` first.
# Get the info for release
release_base_dir = script_dir / mpsd_release
if not os.path.exists(release_base_dir):
raise FileNotFoundError(
"Release directory does not exist. Run create_dir_structure() first."
)
with os_chdir(release_base_dir):
with os_chdir("spack-environments"):
# Get the branch and commit hash of the spack-environments repo
spe_commit_hash = (
run(["git", "rev-parse", "HEAD"], stdout=subprocess.PIPE, check=True)
spe_branch = (
["git", "rev-parse", "--abbrev-ref", "HEAD"],
stdout=subprocess.PIPE,
check=True,
available_toolchains = os.listdir("toolchains")
return spe_branch, spe_commit_hash, available_toolchains
def prepare_environment(mpsd_release: str, script_dir: Path) -> List[str]:
Create the directory structure for the given MPSD release.
It does the following steps:
Clones the spack-environments repository.
Determines the branch and commit hash of the spack-environments repository
and the available toolchains.
Parameters
----------
mpsd_release : str
The name of the MPSD release to prepare the environment for.
script_dir : pathlib.Path
The base directory to create the release folder and
clone the spack-environments repository into.
Returns
-------
available_toolchains : list
A list of available toolchains for the given MPSD release.
create_dir_structure(mpsd_release, script_dir)
spe_branch, spe_commit_hash, available_toolchains = get_release_info(
mpsd_release, script_dir
)
setup_log_cmd(
mpsd_release, script_dir, spe_branch=spe_branch, spe_commit_hash=spe_commit_hash
)
return available_toolchains
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
def get_native_microarchitecture():
"""Return native microarchitecture.
On MPSD machines, there should be an environment variable "MPSD_MICROARCH".
We try to read that. If it fails, we use the 'archspec cpu' command.
If that fails, we ask the user to install it.
Returns
-------
MPSD_MICROARCH : str
Example
-------
>>> get_native_microarchitecture()
'haswell'
"""
# attempt to get MICRO_ARCH from environment variable (should work on
# MPSD_HPC and MPSD linux laptops). If not defined, return
# "UNKNOWN_MICROARCH"
microarch = os.environ.get("MPSD_MICROARCH", "UNKNOWN_MICROARCH")
# if we have not found the microarchitecture environment variable,
# try calling archspec
if microarch == "UNKNOWN_MICROARCH":
logging.debug(
"Couldn't find MPSD_MICROARCH environment variable. Will try archspec."
)
try:
process = run(["archspec", "cpu"], stdout=subprocess.PIPE, text=True)
except FileNotFoundError as e:
logging.debug(f"Call of 'archspec cpu' failed: {e=}")
# Presumably 'archspec' is not installed.
msg = "Please install archspec, for example via 'pipx install archspec'.\n"
msg += "The command we need to execute is 'archspec cpu'.\n"
msg += "Documentation of package: https://archspec.readthedocs.io/"
logging.error(msg)
sys.exit(1)
else: # we have found archspec and executed it
if process.returncode == 0: # sanity check
microarch = process.stdout.strip()
logging.debug(
f"Found microarchitecture from 'archspec cpu' to be '{microarch}'"
)
assert len(microarch) > 0 # sanity check
else:
raise ValueError(
f"Some error occurred when calling 'archspec cpu': {process=}"
)
# at this point, we have determined the microarchitecture
log_metadata("microarchitecture", microarch)
return microarch
def install_environment(
mpsd_release: str,
toolchains: List[str],
script_dir: Path,
force_reinstall: bool = False,
enable_build_cache: bool = False,
) -> None:
Install the specified MPSD release and toolchains.
The function installs the toolchain to the specified directory, using Spack.
Parameters
----------
mpsd_release : str
A string representing the MPSD release version.
toolchains : list of str
A list of strings representing the toolchains to install
(e.g., "foss2021a-mpi", "global_generic", "ALL").
script_dir : pathlib.Path
A Path object representing the path to the directory where
the release and toolchains will be installed.
force_reinstall : bool, optional
A boolean indicating whether to force a reinstallation
even if the release and toolchains already exist. Defaults to False.
enable_build_cache : bool, optional
A boolean indicating whether to build the build cache
when installing toolchains. Defaults to False.
Raises
------
ValueError
If a requested toolchain is not available in the specified release.
Returns
-------
None
logging.info(
f"Installing release {mpsd_release} with toolchains {toolchains} "
f"to {script_dir}"
release_base_dir = script_dir / mpsd_release
mpsd_microarch = get_native_microarchitecture()
toolchain_dir.mkdir(parents=True, exist_ok=True)
spack_setup_script = release_base_dir / "spack-environments" / "spack_setup.sh"
install_flags = []
install_flags.append("-b")
# run the prepare_environment function
available_toolchains = prepare_environment(mpsd_release, script_dir)
# Ensure that the requested toolchains are available in the release
if toolchains == "ALL":
toolchains = available_toolchains
elif toolchains == "NONE":
# No toolchains requested, so we only create the env and print the
# list of available toolchains
logging.warning(
"No toolchains requested. Available toolchains for release "
f"{mpsd_release} are: \n {available_toolchains}"
print_log = logging.getLogger("print")
print_log.info(f"{available_toolchains=}")
return
for toolchain in toolchains:
if toolchain not in available_toolchains:
# TODO: add to message how toolchains can be found
msg = f"Toolchain '{toolchain}' is not available in release {mpsd_release}."
logging.error(msg)
sys.exit(1)
# Install the toolchains
with os_chdir(toolchain_dir):
# run spack_setup_script with the toolchains as arguments
# if the log folder doesn't exist, create it
if not os.path.exists("logs"):
os.mkdir("logs")
# Set the install log file name from create_log_file_names
build_log_file_name = create_log_file_names(
mpsd_release, mpsd_microarch, "install", toolchain=toolchain
)
build_log_folder = release_base_dir / "logs"
build_log_path = build_log_folder / build_log_file_name
# if logs folder dosent exist, create it
if not os.path.exists(build_log_folder):
os.makedirs(build_log_folder)
logging.info(f"Installing toolchain {toolchain} to {toolchain_dir}")
# log the command
setup_log_cmd(
mpsd_release,
script_dir,
msg=f"installing {toolchain} and logging at {build_log_path}",
f"CMD: bash {spack_setup_script} {' '.join(install_flags)} "
f"{toolchain}"
f"bash {spack_setup_script} {' '.join(install_flags)} {toolchain} 2>&1 "
f"| tee -a {build_log_path} ",
def remove_environment(release, toolchains, target_dir):
msg = f"Removing release {release} with toolchains {toolchains} from {target_dir}"
logging.info(msg)
raise NotImplementedError(msg)
def start_new_environment(release, from_release, target_dir):
"""Start new MPSD software environment version."""
msg = f"Starting new release {release} from {from_release} to {target_dir}"
logging.info(msg)
raise NotImplementedError(msg)
def environment_status(mpsd_release, script_dir):
msg = f"Showing status of release {release} in {script_dir}"
print(msg)
# raise NotImplementedError(msg)
release_base_dir = script_dir / mpsd_release
os.environ.get("MPSD_OS", "UNKNOWN_OS")
mpsd_microarch = os.environ.get("MPSD_MICROARCH", "UNKNOWN_MICROARCH")
toolchain_dir = release_base_dir / mpsd_microarch
spack_dir = toolchain_dir / "spack"
if not spack_dir.exists():
raise ValueError(
f"Release {mpsd_release} has not been completly installed yet, try reinstalling it (spack directory not found)"
)
with os_chdir(spack_dir):
parser = argparse.ArgumentParser(description=about_tool)
parser.add_argument(
"--log",
"-l",
dest="loglevel",
choices=["warning", "info", "debug"],
required=False,
default="warning",
help="Set the log level",
)
subparsers = parser.add_subparsers(
dest="action", title="actions", description="valid actions", required=True
)
subparsers.required = True
list_of_cmds = [
("prepare", "Prepare the environment for installation on the disk"),
("install", "Install a software environment"),
("reinstall", "Reinstall a software environment"),
("remove", "Remove a software environment or toolchains from an environment"),
("start-new", "Start a new software environment version"),
("status", "Show the status of the software environment"),
]
for cmd, help_text in list_of_cmds:
subp = subparsers.add_parser(cmd, help=help_text)
if cmd == "start-new":
subp.add_argument(
"--from-release",
dest="from_release",
type=str,
required=True,
help="Release version to start from",
)
subp.add_argument(
"--to-release",
dest="to_release",
type=str,
required=True,
help="Release version to create",
)
"release",
type=str,
help="Release version to prepare, install, reinstall or remove",
if cmd in ["install", "reinstall", "remove"]:
tool_chain_help = (
f"Pass a list of toolchains to command {cmd}. "
"Use '--toolchains ALL' to "
f"{cmd} all toolchains. If '--toolchain' is not "
"specified, list available toolchains for the release "
"(after environment has been prepared if not done yet)."
)
"--toolchains", # first option defines attribute
# name `args.toolchains` in `args = parser_args()`
"--toolchain", # allow singular as alternative
# (-> creates attribute `args.toolchains` if used)
dest="toolchains",
default="NONE",
help=tool_chain_help,
)
subp.add_argument(
"--enable-build-cache",
action="store_true",
"Enable Spack build cache. Useful for reinstallation but "
"consumes time and disk space."
# Carry out the action
args = parser.parse_args()
# target dir is the place where this script exists. the
script_dir = Path(os.path.dirname(os.path.realpath(__file__)))
get_installer_log_file_path(args.release, args.action, script_dir),
remove_environment(args.release, args.toolchains, script_dir)
start_new_environment(args.from_release, args.to_release, script_dir)
install_environment(
args.release, args.toolchains, script_dir, False, args.enable_build_cache
)
elif args.action == "status":
environment_status(args.release, script_dir)