Skip to content

Commit 4a4bd90

Browse files
scripts: west_commands: extend build.dir-fmt format args
build.dir-fmt format string arguments are extended. New format args are 'west_topdir' (absolute path to the west workspace directory) and 'source_dir_ws' (relative path of the source directory to west_topdir). Signed-off-by: Thorsten Klein <[email protected]>
1 parent ab2622f commit 4a4bd90

File tree

5 files changed

+250
-12
lines changed

5 files changed

+250
-12
lines changed

doc/develop/west/build-flash-debug.rst

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -393,12 +393,19 @@ You can :ref:`configure <west-config-cmd>` ``west build`` using these options.
393393
* - ``build.dir-fmt``
394394
- String, default ``build``. The build folder format string, used by
395395
west whenever it needs to create or locate a build folder. The currently
396-
available arguments are:
396+
supported substitution arguments are:
397397

398+
- ``west_topdir``: The absolute path to the west workspace, as
399+
returned by the ``west_topdir`` command
398400
- ``board``: The board name
399-
- ``source_dir``: The relative path from the current working directory
400-
to the source directory. If the current working directory is inside
401-
the source directory this will be set to an empty string.
401+
- ``source_dir``: Path to the CMake source directory, relative to the
402+
current working directory. If the current working directory is
403+
inside the source directory, this is an empty string. If no source
404+
directory is specified, it defaults to current working directory.
405+
- ``source_dir_ws``: Path to the CMake source directory relative to
406+
``west_topdir`` (if it is inside the workspace). Otherwise, it is
407+
relative to the filesystem root (``/`` on Unix, respectively
408+
``C:/`` on Windows).
402409
- ``app``: The name of the source directory.
403410
* - ``build.generator``
404411
- String, default ``Ninja``. The `CMake Generator`_ to use to create a

scripts/west_commands/build.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -287,11 +287,11 @@ def _find_board(self):
287287
if board is not None:
288288
return (board, origin)
289289

290-
if self.args.board:
290+
if getattr(self.args, 'board', None):
291291
board, origin = self.args.board, 'command line'
292292
elif 'BOARD' in os.environ:
293293
board, origin = os.environ['BOARD'], 'env'
294-
elif self.config_board is not None:
294+
elif getattr(self, 'config_board', None) and self.config_board is not None:
295295
board, origin = self.config_board, 'configfile'
296296
return board, origin
297297

@@ -456,16 +456,33 @@ def _update_cache(self):
456456
with contextlib.suppress(FileNotFoundError):
457457
self.cmake_cache = CMakeCache.from_build_dir(self.build_dir)
458458

459+
def _get_dir_fmt_context(self):
460+
west_top_dir = west_topdir()
461+
source_dir = pathlib.Path(self._find_source_dir())
462+
app = source_dir.name
463+
context = {
464+
"west_topdir" : str(west_top_dir),
465+
"source_dir" : str(source_dir),
466+
"app" : app,
467+
}
468+
if source_dir.is_relative_to(west_top_dir):
469+
context['source_dir_ws'] = str(source_dir.relative_to(west_top_dir))
470+
else:
471+
context['source_dir_ws'] = str(source_dir.relative_to(source_dir.anchor))
472+
board, _ = self._find_board()
473+
if board:
474+
context['board'] = board
475+
self.dbg(f'context: {context}', level=Verbosity.DBG_EXTREME)
476+
return context
477+
459478
def _setup_build_dir(self):
460479
# Initialize build_dir and created_build_dir attributes.
461480
# If we created the build directory, we must run CMake.
462481
self.dbg('setting up build directory', level=Verbosity.DBG_EXTREME)
463482
# The CMake Cache has not been loaded yet, so this is safe
464-
board, _ = self._find_board()
465-
source_dir = self._find_source_dir()
466-
app = os.path.split(source_dir)[1]
467-
build_dir = find_build_dir(self.args.build_dir, board=board,
468-
source_dir=source_dir, app=app)
483+
484+
context = self._get_dir_fmt_context()
485+
build_dir = find_build_dir(self.args.build_dir, **context)
469486
if not build_dir:
470487
self.die('Unable to determine a default build folder. Check '
471488
'your build.dir-fmt configuration option')

scripts/west_commands/build_helpers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,9 @@ def find_build_dir(dir, guess=False, **kwargs):
100100
else:
101101
cwd = os.getcwd()
102102
default = config.get('build', 'dir-fmt', fallback=DEFAULT_BUILD_DIR)
103-
default = _resolve_build_dir(default, guess, cwd, **kwargs)
104103
log.dbg(f'config dir-fmt: {default}', level=log.VERBOSE_EXTREME)
104+
default = _resolve_build_dir(default, guess, cwd, **kwargs)
105+
log.dbg(f'config dir-fmt (resolved): {default}', level=log.VERBOSE_EXTREME)
105106
if default and is_zephyr_build(default):
106107
build_dir = default
107108
elif is_zephyr_build(cwd):
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# Copyright (c) 2018 Nordic Semiconductor ASA
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
import configparser
6+
import copy
7+
import os
8+
from argparse import Namespace
9+
from pathlib import Path
10+
11+
import pytest
12+
from build import Build
13+
14+
ROOT = Path(Path.cwd().anchor)
15+
TEST_CWD = ROOT / 'path' / 'to' / 'my' / 'current' / 'cwd'
16+
TEST_CWD_RELATIVE_TO_ROOT = TEST_CWD.relative_to(ROOT)
17+
18+
19+
def setup_test_build(monkeypatch, test_args=None):
20+
# mock configparser read method to keep tests independent from actual configs
21+
monkeypatch.setattr(configparser.ConfigParser, 'read', lambda self, filenames: None)
22+
# mock os.makedirs to be independent from actual filesystem
23+
monkeypatch.setattr('os.makedirs', lambda *a, **kw: None)
24+
# mock west_topdir so that tests run independent from user machine
25+
monkeypatch.setattr('build.west_topdir', lambda *a, **kw: str(west_topdir))
26+
# mock os.getcwd and Path.cwd to use TEST_CWD
27+
monkeypatch.setattr('os.getcwd', lambda *a, **kw: str(TEST_CWD))
28+
monkeypatch.setattr('pathlib.Path.cwd', lambda *a, **kw: TEST_CWD)
29+
# mock os.environ to ignore all environment variables during test
30+
monkeypatch.setattr('os.environ', {})
31+
32+
# set up Build
33+
b = Build()
34+
35+
# apply test args
36+
b.args = copy.copy(DEFAULT_TEST_ARGS)
37+
if test_args:
38+
for k, v in vars(test_args).items():
39+
setattr(b.args, k, v)
40+
41+
return b
42+
43+
44+
# Use a hardcoded west_topdir to ensure that the tests run independently
45+
# from the actual user machine
46+
west_topdir = ROOT / 'any' / 'west' / 'workspace'
47+
48+
DEFAULT_TEST_ARGS = Namespace(
49+
board=None, source_dir=west_topdir / 'subdir' / 'project' / 'app', build_dir=None
50+
)
51+
52+
TEST_CASES_GET_DIR_FMT_CONTEXT = [
53+
# (test_args, source_dir, expected)
54+
# fallback to cwd in default case
55+
(
56+
{},
57+
None,
58+
{
59+
'west_topdir': str(west_topdir),
60+
'app': TEST_CWD.name,
61+
'source_dir': str(TEST_CWD),
62+
'source_dir_ws': str(TEST_CWD_RELATIVE_TO_ROOT),
63+
},
64+
),
65+
# check for correct source_dir and source_dir_ws (if inside west_topdir)
66+
(
67+
{},
68+
west_topdir / 'my' / 'project',
69+
{
70+
'west_topdir': str(west_topdir),
71+
'app': 'project',
72+
'source_dir': str(west_topdir / 'my' / 'project'),
73+
'source_dir_ws': str(Path('my') / 'project'),
74+
},
75+
),
76+
# check for correct source_dir and source_dir_ws (if outside west_topdir)
77+
(
78+
{},
79+
ROOT / 'path' / 'to' / 'my-project',
80+
{
81+
'west_topdir': str(west_topdir),
82+
'app': 'my-project',
83+
'source_dir': str(ROOT / 'path' / 'to' / 'my-project'),
84+
'source_dir_ws': str(Path('path') / 'to' / 'my-project'),
85+
},
86+
),
87+
# check for correct board
88+
(
89+
Namespace(board='native_sim'),
90+
None,
91+
{
92+
'board': 'native_sim',
93+
'west_topdir': str(west_topdir),
94+
'app': TEST_CWD.name,
95+
'source_dir': str(TEST_CWD),
96+
'source_dir_ws': str(TEST_CWD_RELATIVE_TO_ROOT),
97+
},
98+
),
99+
]
100+
101+
102+
@pytest.mark.parametrize('test_case', TEST_CASES_GET_DIR_FMT_CONTEXT)
103+
def test_get_dir_fmt_context(monkeypatch, test_case):
104+
# extract data from the test case
105+
test_args, source_dir, expected = test_case
106+
107+
# set up and run _get_dir_fmt_context
108+
b = setup_test_build(monkeypatch, test_args)
109+
b.args.source_dir = source_dir
110+
actual = b._get_dir_fmt_context()
111+
assert expected == actual
112+
113+
114+
TEST_CASES_BUILD_DIR = [
115+
# (config_build, test_args, expected)
116+
# default build directory if no args and dir-fmt are specified
117+
({}, None, Path('build')),
118+
# build_dir from args should always be preferred (if it is specified)
119+
({}, Namespace(build_dir='from-args'), Path('from-args')),
120+
({'dir-fmt': 'from-dir-fmt'}, Namespace(build_dir='from-args'), Path('from-args')),
121+
# build_dir is determined by resolving dir-fmt format string
122+
# must be able to resolve a simple string
123+
({'dir-fmt': 'from-dir-fmt'}, None, 'from-dir-fmt'),
124+
# must be able to resolve west_topdir
125+
({'dir-fmt': '{west_topdir}/build'}, None, west_topdir / 'build'),
126+
# must be able to resolve app
127+
({'dir-fmt': '{app}'}, None, 'app'),
128+
# must be able to resolve source_dir (when it is inside west workspace)
129+
(
130+
{'dir-fmt': '{source_dir}'},
131+
None,
132+
# source_dir resolves to relative path (relative to cwd), so build
133+
# directory is depending on cwd and ends up outside 'build'
134+
os.path.relpath(DEFAULT_TEST_ARGS.source_dir, TEST_CWD),
135+
),
136+
# source_dir dir is outside west workspace, so it is absolute path
137+
(
138+
{'dir-fmt': '{source_dir}'},
139+
Namespace(source_dir=ROOT / 'outside' / 'living' / 'app'),
140+
os.path.relpath(ROOT / 'outside' / 'living' / 'app', TEST_CWD),
141+
),
142+
# must be able to resolve source_dir_ws (when source_dir is inside west workspace)
143+
({'dir-fmt': '{source_dir_ws}'}, None, Path('subdir') / 'project' / 'app'),
144+
# must be able to resolve source_dir_ws (when source_dir is outside west workspace)
145+
(
146+
{'dir-fmt': 'build/{source_dir_ws}'},
147+
Namespace(source_dir=ROOT / 'outside' / 'living' / 'app'),
148+
Path('build') / 'outside' / 'living' / 'app',
149+
),
150+
# must be able to resolve board (must be specified)
151+
({'dir-fmt': '{board}'}, Namespace(board='native_sim'), 'native_sim'),
152+
]
153+
154+
155+
@pytest.mark.parametrize('test_case', TEST_CASES_BUILD_DIR)
156+
def test_dir_fmt(monkeypatch, test_case):
157+
# extract data from the test case
158+
config_build, test_args, expected = test_case
159+
160+
# apply given config_build
161+
config = configparser.ConfigParser()
162+
config.add_section("build")
163+
for k, v in config_build.items():
164+
config.set('build', k, v)
165+
monkeypatch.setattr("build_helpers.config", config)
166+
monkeypatch.setattr("build.config", config)
167+
168+
# set up and run _setup_build_dir
169+
b = setup_test_build(monkeypatch, test_args)
170+
b._setup_build_dir()
171+
172+
# check for expected build-dir
173+
assert os.path.abspath(expected) == b.build_dir
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Copyright (c) 2018 Nordic Semiconductor ASA
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
from pathlib import Path
6+
7+
import pytest
8+
from build_helpers import _resolve_build_dir
9+
10+
cwd = Path.cwd()
11+
root = Path(cwd.anchor)
12+
TEST_CWD = root / 'path' / 'to' / 'my' / 'current' / 'cwd'
13+
14+
TEST_CASES_RESOLVE_BUILD_DIR = [
15+
# (fmt, kwargs, expected)
16+
# simple string (no format string)
17+
('simple/string', {}, 'simple/string'),
18+
# source_dir is inside cwd
19+
('{source_dir}', {'source_dir': TEST_CWD / 'subdir'}, 'subdir'),
20+
# source_dir is outside cwd
21+
('{source_dir}', {'source_dir': TEST_CWD / '..' / 'subdir'}, str(Path('..') / 'subdir')),
22+
# cwd is inside source dir
23+
('{source_dir}', {'source_dir': TEST_CWD / '..'}, ''),
24+
# source dir not resolvable by default
25+
('{source_dir}', {}, None),
26+
# invalid format arg
27+
('{invalid}', {}, None),
28+
# app is defined by default
29+
('{app}', {}, None),
30+
]
31+
32+
33+
@pytest.mark.parametrize('test_case', TEST_CASES_RESOLVE_BUILD_DIR)
34+
def test_resolve_build_dir(test_case):
35+
fmt, kwargs, expected = test_case
36+
37+
# test both guess=True and guess=False
38+
for guess in [True, False]:
39+
actual = _resolve_build_dir(cwd=TEST_CWD, guess=guess, fmt=fmt, **kwargs)
40+
assert actual == expected

0 commit comments

Comments
 (0)