1# Copyright 2018 (c) Foundries.io.
2#
3# SPDX-License-Identifier: Apache-2.0
4
5'''Common definitions for building Zephyr applications.
6
7This provides some default settings and convenience wrappers for
8building Zephyr applications needed by multiple commands.
9
10See build.py for the build command itself.
11'''
12
13import os
14import sys
15from pathlib import Path
16
17import zcmake
18from west import log
19from west.configuration import config
20from west.util import escapes_directory
21
22# Domains.py must be imported from the pylib directory, since
23# twister also uses the implementation
24script_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
25sys.path.insert(0, os.path.join(script_dir, "pylib/build_helpers/"))
26from domains import Domains  # noqa: E402
27
28DEFAULT_BUILD_DIR = 'build'
29'''Name of the default Zephyr build directory.'''
30
31DEFAULT_CMAKE_GENERATOR = 'Ninja'
32'''Name of the default CMake generator.'''
33
34FIND_BUILD_DIR_DESCRIPTION = f'''\
35If the build directory is not given, the default is {DEFAULT_BUILD_DIR}/ unless the
36build.dir-fmt configuration variable is set. The current directory is
37checked after that. If either is a Zephyr build directory, it is used.
38'''
39
40def _resolve_build_dir(fmt, guess, cwd, **kwargs):
41    # Remove any None values, we do not want 'None' as a string
42    kwargs = {k: v for k, v in kwargs.items() if v is not None}
43    # Check if source_dir is below cwd first
44    source_dir = kwargs.get('source_dir')
45    if source_dir:
46        if escapes_directory(cwd, source_dir):
47            kwargs['source_dir'] = os.path.relpath(source_dir, cwd)
48        else:
49            # no meaningful relative path possible
50            kwargs['source_dir'] = ''
51
52    try:
53        return fmt.format(**kwargs)
54    except KeyError:
55        if not guess:
56            return None
57
58    # Guess the build folder by iterating through all sub-folders from the
59    # root of the format string and trying to resolve. If resolving fails,
60    # proceed to iterate over subfolders only if there is a single folder
61    # present on each iteration.
62    parts = Path(fmt).parts
63    b = Path('.')
64    for p in parts:
65        # default to cwd in the first iteration
66        curr = b
67        b = b.joinpath(p)
68        try:
69            # if fmt is an absolute path, the first iteration will always
70            # resolve '/'
71            b = Path(str(b).format(**kwargs))
72        except KeyError:
73            # Missing key, check sub-folders and match if a single one exists
74            while True:
75                if not curr.exists():
76                    return None
77                dirs = [f for f in curr.iterdir() if f.is_dir()]
78                if len(dirs) != 1:
79                    return None
80                curr = dirs[0]
81                if is_zephyr_build(str(curr)):
82                    return str(curr)
83    return str(b)
84
85def find_build_dir(dir, guess=False, **kwargs):
86    '''Heuristic for finding a build directory.
87
88    The default build directory is computed by reading the build.dir-fmt
89    configuration option, defaulting to DEFAULT_BUILD_DIR if not set. It might
90    be None if the build.dir-fmt configuration option is set but cannot be
91    resolved.
92    If the given argument is truthy, it is returned. Otherwise, if
93    the default build folder is a build directory, it is returned.
94    Next, if the current working directory is a build directory, it is
95    returned. Finally, the default build directory is returned (may be None).
96    '''
97
98    if dir:
99        build_dir = dir
100    else:
101        cwd = os.getcwd()
102        default = config.get('build', 'dir-fmt', fallback=DEFAULT_BUILD_DIR)
103        default = _resolve_build_dir(default, guess, cwd, **kwargs)
104        log.dbg(f'config dir-fmt: {default}', level=log.VERBOSE_EXTREME)
105        if default and is_zephyr_build(default):
106            build_dir = default
107        elif is_zephyr_build(cwd):
108            build_dir = cwd
109        else:
110            build_dir = default
111    log.dbg(f'build dir: {build_dir}', level=log.VERBOSE_EXTREME)
112    if build_dir:
113        return os.path.abspath(build_dir)
114    else:
115        return None
116
117def is_zephyr_build(path):
118    '''Return true if and only if `path` appears to be a valid Zephyr
119    build directory.
120
121    "Valid" means the given path is a directory which contains a CMake
122    cache with a 'ZEPHYR_BASE' or 'ZEPHYR_TOOLCHAIN_VARIANT' variable.
123
124    (The check for ZEPHYR_BASE introduced sometime after Zephyr 2.4 to
125    fix https://github.com/zephyrproject-rtos/zephyr/issues/28876; we
126    keep support for the second variable around for compatibility with
127    versions 2.2 and earlier, which didn't have ZEPHYR_BASE in cache.
128    The cached ZEPHYR_BASE was added in
129    https://github.com/zephyrproject-rtos/zephyr/pull/23054.)
130    '''
131    try:
132        cache = zcmake.CMakeCache.from_build_dir(path)
133    except FileNotFoundError:
134        cache = {}
135
136    if 'ZEPHYR_BASE' in cache or 'ZEPHYR_TOOLCHAIN_VARIANT' in cache:
137        log.dbg(f'{path} is a zephyr build directory',
138                level=log.VERBOSE_EXTREME)
139        return True
140
141    log.dbg(f'{path} is NOT a valid zephyr build directory',
142            level=log.VERBOSE_EXTREME)
143    return False
144
145
146def load_domains(path):
147    '''Load domains from a domains.yaml.
148
149    If domains.yaml is not found, then a single 'app' domain referring to the
150    top-level build folder is created and returned.
151    '''
152    domains_file = Path(path) / 'domains.yaml'
153
154    if not domains_file.is_file():
155        return Domains.from_yaml(f'''\
156default: app
157build_dir: {path}
158domains:
159  - name: app
160    build_dir: {path}
161flash_order:
162  - app
163''')
164
165    return Domains.from_file(domains_file)
166