"""Utilities for invoking CMake
See the source code on Github: `mltk/utils/cmake.py <https://github.com/siliconlabs/mltk/blob/master/mltk/utils/cmake.py>`_
"""
from typing import List, Union, Dict
import sys
import os
import logging
import re
from cmake import CMAKE_BIN_DIR
CMAKE_BIN_DIR = CMAKE_BIN_DIR.replace('\\', '/')
from mltk import MLTK_ROOT_DIR
from .shell_cmd import run_shell_cmd
from .path import clean_directory, create_tempdir
from .system import get_current_os
from .logger import DummyLogger, make_filelike
from .python import as_list
PLATFORM_TOOLCHAIN_MAPPING = {
'windows' : 'gcc/windows/win64_toolchain.cmake',
'linux' : 'gcc/linux/linux_toolchain.cmake',
'osx' : 'gcc/osx/osx_toolchain.cmake',
'brd2204' : 'gcc/arm/arm_toolchain.cmake',
'brd4186' : 'gcc/arm/arm_toolchain.cmake',
'brd2601' : 'gcc/arm/arm_toolchain.cmake',
'brd4166' : 'gcc/arm/arm_toolchain.cmake',
'brd4401' : 'gcc/arm/arm_toolchain.cmake',
'brd2705' : 'gcc/arm/arm_toolchain.cmake',
}
[docs]def build_mltk_target(
target:str=None,
mltk_target:str=None,
additional_variables:Union[List[str],Dict[str,str]]=None,
debug:bool=False,
clean:bool=False,
source_dir:str=None,
build_dir:str=None,
build_subdir:str=None,
platform:str=None,
logger:logging.Logger=None,
verbose:bool=False,
jobs:int=None,
accelerator:str=None,
use_user_options:bool=False,
config_only:bool=False,
build_only:bool=False
) -> str:
"""Build an MLTK CMake target
Args:
target: Name of CMake target to build
mltk_target: Name of MLTK_TARGET, if omitted use target
additional_variables: List or dictionary of additional CMake variables to add to the build command
debug: If true then build with debugging (i.e. full symbols and no optimization)
clean: Clean the build directory before building
source_dir: Path to source directory, if omitted use mltk root directory
build_dir: Path to build directory, if omitted use <temp dir>/mltk/build
build_subdir: Name of sub build directory, if omitted use target name
platform: Build platform, if omitted use current OS
logger: Optional python logger
verbose: Enable verbose logging while building
accelerator: Name of accelerator to use for TFLITE_MICRO_ACCELERATOR CMake variable
jobs: Number of parallel build jobs
use_user_options: Use the user_options.cmake in the source directory. Default is to IGNORE user_options.cmake
config_only: Only configure the CMake project, do not build it
build_only: Only build the target, do not configure it first. In this case, the project must have been previosly configured
Returns:
The path to the build directory
"""
logger = logger or DummyLogger()
make_filelike(logger)
mltk_target = mltk_target or target
additional_variables = parse_variables(additional_variables)
if source_dir is None:
source_dir = MLTK_ROOT_DIR
source_dir = source_dir.replace('\\', '/')
if debug:
build_type = 'Debug'
else:
build_type = 'Release'
platform = platform or get_current_os()
build_dir = get_build_directory(
platform=platform,
target=target,
debug=debug,
build_dir=build_dir,
build_subdir=build_subdir
)
if clean:
clean_directory(build_dir)
if platform not in PLATFORM_TOOLCHAIN_MAPPING:
support_platforms = list(PLATFORM_TOOLCHAIN_MAPPING.keys())
raise ValueError(f'Unsupported platform {platform}, supported platforms are: {support_platforms}')
toolchain_file = PLATFORM_TOOLCHAIN_MAPPING[platform]
python_dir = os.path.dirname(os.path.dirname(sys.executable)).replace('\\', '/')
cmake_vars = parse_variables([
#'CMAKE_OBJECT_PATH_MAX:STRING=1024',
f'CMAKE_TOOLCHAIN_FILE:FILEPATH={MLTK_ROOT_DIR}/cpp/tools/toolchains/{toolchain_file}',
f'CMAKE_BUILD_TYPE:STRING={build_type}',
f'MLTK_PYTHON_VENV_DIR:FILEPATH={python_dir}',
f'MLTK_PLATFORM_NAME:STRING={platform}',
f'MLTK_TARGET:STRING={mltk_target}'
])
if not use_user_options:
cmake_vars['MLTK_NO_USER_OPTIONS'] = 'ON'
if verbose:
cmake_vars['MLTK_CMAKE_LOG_LEVEL:STRING'] = 'debug'
if accelerator:
cmake_vars['TFLITE_MICRO_ACCELERATOR:STRING'] = accelerator.lower()
for key, value in additional_variables.items():
value = str(value)
# These args do not go through the shell parser
# so they do not need to be wrapped with double-quotes
if value.startswith('"') and value.endswith('"'):
value = value[1:-1]
cmake_vars[key] = value
cmake_vars_args = []
for key, value in cmake_vars.items():
cmake_vars_args.append(f'-D{key}={value}')
if not build_only:
cmd = [
f'{CMAKE_BIN_DIR}/cmake',
'--no-warn-unused-cli',
'-Wno-dev',
*cmake_vars_args,
f'-S{source_dir}',
f'-B{build_dir}',
'-G Ninja'
]
cmd_str = " ".join(cmd)
logger.info(f'Configuring CMake project:\n{cmd_str}')
retcode, _ = run_shell_cmd(
cmd=cmd,
outfile=logger,
)
if retcode != 0:
raise RuntimeError(f'Failed to configure CMake project using command:\n{cmd_str}')
cmd =[
f'{CMAKE_BIN_DIR}/cmake',
'--build', build_dir,
'--config', build_type,
'--target', target
]
if verbose or jobs:
cmd.append('--')
if verbose:
cmd.extend(['-d', 'keeprsp'])
cmd.append('-v')
if jobs:
cmd.extend(['-j', str(jobs)])
if not config_only:
cmd_str = " ".join(cmd)
logger.info(f'Building CMake project:\n{cmd_str}')
retcode, _ = run_shell_cmd(
cmd=cmd,
outfile=logger
)
if retcode != 0:
raise RuntimeError(f'Failed to build CMake project using command:\n{cmd_str}')
return build_dir
[docs]def invoke_mltk_target(
target:str,
build_target:str=None,
debug:bool=False,
build_dir:str=None,
build_subdir:str=None,
platform:str=None,
logger:logging.Logger=None,
verbose:bool=False,
) -> str:
"""Invoke an MLTK CMake target"""
logger = logger or DummyLogger()
make_filelike(logger)
platform = platform or get_current_os()
build_dir = get_build_directory(
platform=platform,
target=build_target,
debug=debug,
build_dir=build_dir,
build_subdir=build_subdir
)
cmd = [f'{CMAKE_BIN_DIR}/cmake']
cmd.extend(['--build', build_dir])
cmd.extend(['--target', target])
if verbose or (logger is not None and hasattr(logger, 'verbose') and logger.verbose):
cmd.append('-v')
logger.info(f'Invoking {" ".join(cmd)}')
retcode, retval = run_shell_cmd(
cmd=cmd,
outfile=logger,
)
if retcode != 0:
raise RuntimeError('Failed to invoke CMake target')
return retval
[docs]def get_build_directory(
platform:str=None,
target:str=None,
debug:bool=False,
build_dir:str=None,
build_subdir:str=None,
) -> str:
if build_dir is None:
build_dir = create_tempdir('build')
build_dir = build_dir.replace('\\', '/')
if build_subdir is False:
return build_dir
platform = platform or get_current_os()
platform = platform.lower()
if debug:
build_type = 'Debug'
else:
build_type = 'Release'
build_subdir = build_subdir or target
build_dir += f'/{build_subdir}/{platform}/{build_type}'
return build_dir
[docs]def parse_variables(
cmake_variables:Union[List[str],Dict[str,str]]
) -> Dict[str,str]:
"""Convert a list or dictionary of CMake variables into a dictionary"""
if not cmake_variables:
return {}
if isinstance(cmake_variables, (list,str)):
var_re = re.compile(r'(.+)\s*=\s*(.*)')
cmake_variables = as_list(cmake_variables)
retval = {}
for v in cmake_variables:
if not v:
continue
match = var_re.match(v)
if not match:
raise ValueError(f'Invalid CMake variable: {v}, must be of form: <name>=<value>')
key = match.group(1)
value = match.group(2)
if key.startswith('-D'):
key = key[2:]
retval[key] = value
return retval
else:
return cmake_variables