scripts: west commands to support --domain

This commit extends the west commands build, flash, and debug to support
--domain when having multiple domains (images) defined in a domains.yaml
build file.

The domains.yaml uses the following yaml format to specify the
build directory of each domain in the multi image build:
> default: <domain-n>
> domains:
>   <domain-1>:
>     build_dir: <build_dir-domain-1>
>   <domain-2>:
>     build_dir: <build_dir-domain-2>
>   ...

`west <build|flash|debug>` has been extended to support
`--domain <domain>`.

`west build` calls CMake to create the build system, and if `--domain`
is given, then the build tool will be invoked afterwards for the
specified domain.

`west flash` will default flash all domains, but `--domain <domain>`
argument can be used to select a specific domain to flash, for example:
> west flash --domain mcuboot

`west debug` only a single domain can be debugged at any given time.
If `--domain` is not specified, then the default domain specified in the
domains.yml file will be used.
Users can still select a different domain, for example with:
> west debug --domain mcuboot

Signed-off-by: Torsten Rasmussen <Torsten.Rasmussen@nordicsemi.no>
This commit is contained in:
Torsten Rasmussen 2021-11-22 10:29:56 +01:00 committed by Carles Cufí
parent 5fe5d6b43d
commit 8408af6d7c
5 changed files with 213 additions and 14 deletions

View file

@ -12,7 +12,7 @@ import yaml
from west import log
from west.configuration import config
from zcmake import DEFAULT_CMAKE_GENERATOR, run_cmake, run_build, CMakeCache
from build_helpers import is_zephyr_build, find_build_dir, \
from build_helpers import is_zephyr_build, find_build_dir, load_domains, \
FIND_BUILD_DIR_DESCRIPTION
from zephyr_ext_common import Forceable
@ -115,6 +115,9 @@ class Build(Forceable):
help='force a cmake run')
group.add_argument('--cmake-only', action='store_true',
help="just run cmake; don't build (implies -c)")
group.add_argument('--domain', action='append',
help='''execute build tool (make or ninja) only for
given domain''')
group.add_argument('-t', '--target',
help='''run build system target TARGET
(try "-t usage")''')
@ -201,8 +204,9 @@ class Build(Forceable):
self._sanity_check()
self._update_cache()
self.domains = load_domains(self.build_dir)
self._run_build(args.target)
self._run_build(args.target, args.domain)
def _find_board(self):
board, origin = None, None
@ -464,7 +468,7 @@ class Build(Forceable):
config_sysbuild = config_getboolean('sysbuild', False)
if self.args.sysbuild or (config_sysbuild and not self.args.no_sysbuild):
cmake_opts.extend(['-S{}'.format(SYSBUILD_PROJ_DIR),
'-DAPP_DIR={}'.format(self.source_dir)])
'-DAPP_DIR:PATH={}'.format(self.source_dir)])
else:
# self.args.no_sysbuild == True or config sysbuild False
cmake_opts.extend(['-S{}'.format(self.source_dir)])
@ -499,7 +503,7 @@ class Build(Forceable):
'-P', cache['ZEPHYR_BASE'] + '/cmake/pristine.cmake']
run_cmake(cmake_args, cwd=self.build_dir, dry_run=self.args.dry_run)
def _run_build(self, target):
def _run_build(self, target, domain):
if target:
_banner('running target {}'.format(target))
elif self.run_cmake:
@ -511,8 +515,23 @@ class Build(Forceable):
if self.args.verbose:
self._append_verbose_args(extra_args,
not bool(self.args.build_opt))
run_build(self.build_dir, extra_args=extra_args,
dry_run=self.args.dry_run)
domains = load_domains(self.build_dir)
build_dir_list = []
if domain is None:
# If no domain is specified, we just build top build dir as that
# will build all domains.
build_dir_list = [domains.get_top_build_dir()]
else:
_banner('building domain(s): {}'.format(' '.join(domain)))
domain_list = domains.get_domains(domain)
for d in domain_list:
build_dir_list.append(d.build_dir)
for b in build_dir_list:
run_build(b, extra_args=extra_args,
dry_run=self.args.dry_run)
def _append_verbose_args(self, extra_args, add_dashes):
# These hacks are only needed for CMake versions earlier than

View file

@ -16,6 +16,7 @@ from pathlib import Path
from west import log
from west.configuration import config
from west.util import escapes_directory
from domains import Domains
DEFAULT_BUILD_DIR = 'build'
'''Name of the default Zephyr build directory.'''
@ -133,3 +134,19 @@ def is_zephyr_build(path):
log.dbg(f'{path} is NOT a valid zephyr build directory',
level=log.VERBOSE_EXTREME)
return False
def load_domains(path):
'''Load domains from a domains.yaml.
If domains.yaml is not found, then a single 'app' domain referring to the
top-level build folder is created and returned.
'''
domains_file = Path(path) / 'domains.yaml'
if not domains_file.is_file():
return Domains.from_data({'default': 'app',
'build_dir': path,
'domains': [{'name': 'app', 'build_dir': path}]})
return Domains.from_file(domains_file)

View file

@ -0,0 +1,139 @@
# Copyright (c) 2022 Nordic Semiconductor ASA
#
# SPDX-License-Identifier: Apache-2.0
'''Domain handling for west extension commands.
This provides parsing of domains yaml file and creation of objects of the
Domain class.
'''
import yaml
import pykwalify.core
from west import log
DOMAINS_SCHEMA = '''
## A pykwalify schema for basic validation of the structure of a
## domains YAML file.
##
# The domains.yaml file is a simple list of domains from a multi image build
# along with the default domain to use.
type: map
mapping:
default:
required: true
type: str
build_dir:
required: true
type: str
domains:
required: false
type: seq
sequence:
- type: map
mapping:
name:
required: true
type: str
build_dir:
required: true
type: str
'''
schema = yaml.safe_load(DOMAINS_SCHEMA)
class Domains:
def __init__(self, data):
self._domains = []
self._domain_names = []
self._domain_default = []
self._build_dir = data.get('build_dir')
domain_list = data.get('domains')
if not domain_list:
log.wrn("no domains defined; this probably won't work")
for d in domain_list:
domain = Domain(d['name'], d['build_dir'])
self._domains.append(domain)
self._domain_names.append(domain.name)
if domain.name == data['default']:
self._default_domain = domain
@staticmethod
def from_file(domains_file):
'''Load domains from domains.yaml.
Exception raised:
- ``FileNotFoundError`` if the domains file is not found.
'''
try:
with open(domains_file, 'r') as f:
domains = yaml.safe_load(f.read())
except FileNotFoundError:
log.die(f'domains.yaml file not found: {domains_file}')
try:
pykwalify.core.Core(source_data=domains, schema_data=schema)\
.validate()
except pykwalify.errors.SchemaError:
log.die(f'ERROR: Malformed yaml in file: {domains_file}')
return Domains(domains)
@staticmethod
def from_data(domains_data):
'''Load domains from domains dictionary.
'''
return Domains(domains_data)
def get_domains(self, names=None):
ret = []
if not names:
return self._domains
for n in names:
found = False
for d in self._domains:
if n == d.name:
ret.append(d)
found = True
break
# Getting here means the domain was not found.
# Todo: throw an error.
if not found:
log.die(f'domain {n} not found, '
f'valid domains are:', *self._domain_names)
return ret
def get_default_domain(self):
return self._default_domain
def get_top_build_dir(self):
return self._build_dir
class Domain:
def __init__(self, name, build_dir):
self.name = name
self.build_dir = build_dir
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value
@property
def build_dir(self):
return self._build_dir
@build_dir.setter
def build_dir(self, value):
self._build_dir = value

View file

@ -8,7 +8,8 @@
from west.commands import WestCommand
from run_common import add_parser_common, do_run_common
from run_common import add_parser_common, do_run_common, get_build_dir
from build_helpers import load_domains
class Flash(WestCommand):
@ -26,4 +27,6 @@ class Flash(WestCommand):
return add_parser_common(self, parser_adder)
def do_run(self, my_args, runner_args):
do_run_common(self, my_args, runner_args)
build_dir = get_build_dir(my_args)
domains = load_domains(build_dir).get_domains(my_args.domain)
do_run_common(self, my_args, runner_args, domains=domains)

View file

@ -16,7 +16,7 @@ import textwrap
import traceback
from west import log
from build_helpers import find_build_dir, is_zephyr_build, \
from build_helpers import find_build_dir, is_zephyr_build, load_domains, \
FIND_BUILD_DIR_DESCRIPTION
from west.commands import CommandError
from west.configuration import config
@ -104,6 +104,8 @@ def add_parser_common(command, parser_adder=None, parser=None):
help='override default runner from --build-dir')
group.add_argument('--skip-rebuild', action='store_true',
help='do not refresh cmake dependencies first')
group.add_argument('--domain', action='append',
help='execute runner only for given domain')
group = parser.add_argument_group(
'runner configuration',
@ -145,7 +147,7 @@ def add_parser_common(command, parser_adder=None, parser=None):
return parser
def do_run_common(command, user_args, user_runner_args):
def do_run_common(command, user_args, user_runner_args, domains=None):
# This is the main routine for all the "west flash", "west debug",
# etc. commands.
@ -153,13 +155,30 @@ def do_run_common(command, user_args, user_runner_args):
dump_context(command, user_args, user_runner_args)
return
command_name = command.name
build_dir = get_build_dir(user_args)
cache = load_cmake_cache(build_dir, user_args)
board = cache['CACHED_BOARD']
if not user_args.skip_rebuild:
rebuild(command, build_dir, user_args)
if domains is None:
if user_args.domain is None:
# No domains are passed down and no domains specified by the user.
# So default domain will be used.
domains = [load_domains(build_dir).get_default_domain()]
else:
# No domains are passed down, but user has specified domains to use.
# Get the user specified domains.
domains = load_domains(build_dir).get_domains(user_args.domain)
for d in domains:
do_run_common_image(command, user_args, user_runner_args, d.build_dir)
def do_run_common_image(command, user_args, user_runner_args, build_dir=None):
command_name = command.name
if build_dir is None:
build_dir = get_build_dir(user_args)
cache = load_cmake_cache(build_dir, user_args)
board = cache['CACHED_BOARD']
# Load runners.yaml.
yaml_path = runners_yaml_path(build_dir, board)
runners_yaml = load_runners_yaml(yaml_path)
@ -173,7 +192,9 @@ def do_run_common(command, user_args, user_runner_args):
# Set up runner logging to delegate to west.log commands.
logger = logging.getLogger('runners')
logger.setLevel(LOG_LEVEL)
logger.addHandler(WestLogHandler())
if not logger.hasHandlers():
# Only add a runners log handler if none has been added already.
logger.addHandler(WestLogHandler())
# If the user passed -- to force the parent argument parser to stop
# parsing, it will show up here, and needs to be filtered out.