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