Skip to content

Commit 1cfc21e

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 1cfc21e

File tree

4 files changed

+252
-11
lines changed

4 files changed

+252
-11
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: 25 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,34 @@ 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+
# Return a dictionary of build attributes which are used while
461+
# substituting the placeholders in the build.dir-fmt format string.
462+
west_top_dir = west_topdir()
463+
source_dir = pathlib.Path(self._find_source_dir())
464+
app = source_dir.name
465+
board, _ = self._find_board()
466+
context = {
467+
"west_topdir" : str(west_top_dir),
468+
"source_dir" : str(source_dir),
469+
"app" : app,
470+
"board" : board
471+
}
472+
if source_dir.is_relative_to(west_top_dir):
473+
context['source_dir_ws'] = str(source_dir.relative_to(west_top_dir))
474+
else:
475+
context['source_dir_ws'] = str(source_dir.relative_to(source_dir.anchor))
476+
self.dbg(f'dir-fmt context: {context}', level=Verbosity.DBG_EXTREME)
477+
return context
478+
459479
def _setup_build_dir(self):
460480
# Initialize build_dir and created_build_dir attributes.
461481
# If we created the build directory, we must run CMake.
462482
self.dbg('setting up build directory', level=Verbosity.DBG_EXTREME)
463483
# 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)
484+
485+
context = self._get_dir_fmt_context()
486+
build_dir = find_build_dir(self.args.build_dir, **context)
469487
if not build_dir:
470488
self.die('Unable to determine a default build folder. Check '
471489
'your build.dir-fmt configuration option')
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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+
'board': None,
60+
'west_topdir': str(west_topdir),
61+
'app': TEST_CWD.name,
62+
'source_dir': str(TEST_CWD),
63+
'source_dir_ws': str(TEST_CWD_RELATIVE_TO_ROOT),
64+
},
65+
),
66+
# check for correct source_dir and source_dir_ws (if inside west_topdir)
67+
(
68+
{},
69+
west_topdir / 'my' / 'project',
70+
{
71+
'board': None,
72+
'west_topdir': str(west_topdir),
73+
'app': 'project',
74+
'source_dir': str(west_topdir / 'my' / 'project'),
75+
'source_dir_ws': str(Path('my') / 'project'),
76+
},
77+
),
78+
# check for correct source_dir and source_dir_ws (if outside west_topdir)
79+
(
80+
{},
81+
ROOT / 'path' / 'to' / 'my-project',
82+
{
83+
'board': None,
84+
'west_topdir': str(west_topdir),
85+
'app': 'my-project',
86+
'source_dir': str(ROOT / 'path' / 'to' / 'my-project'),
87+
'source_dir_ws': str(Path('path') / 'to' / 'my-project'),
88+
},
89+
),
90+
# check for correct board
91+
(
92+
Namespace(board='native_sim'),
93+
None,
94+
{
95+
'board': 'native_sim',
96+
'west_topdir': str(west_topdir),
97+
'app': TEST_CWD.name,
98+
'source_dir': str(TEST_CWD),
99+
'source_dir_ws': str(TEST_CWD_RELATIVE_TO_ROOT),
100+
},
101+
),
102+
]
103+
104+
105+
@pytest.mark.parametrize('test_case', TEST_CASES_GET_DIR_FMT_CONTEXT)
106+
def test_get_dir_fmt_context(monkeypatch, test_case):
107+
# extract data from the test case
108+
test_args, source_dir, expected = test_case
109+
110+
# set up and run _get_dir_fmt_context
111+
b = setup_test_build(monkeypatch, test_args)
112+
b.args.source_dir = source_dir
113+
actual = b._get_dir_fmt_context()
114+
assert expected == actual
115+
116+
117+
TEST_CASES_BUILD_DIR = [
118+
# (config_build, test_args, expected)
119+
# default build directory if no args and dir-fmt are specified
120+
({}, None, Path('build')),
121+
# build_dir from args should always be preferred (if it is specified)
122+
({}, Namespace(build_dir='from-args'), Path('from-args')),
123+
({'dir-fmt': 'from-dir-fmt'}, Namespace(build_dir='from-args'), Path('from-args')),
124+
# build_dir is determined by resolving dir-fmt format string
125+
# must be able to resolve a simple string
126+
({'dir-fmt': 'from-dir-fmt'}, None, 'from-dir-fmt'),
127+
# must be able to resolve west_topdir
128+
({'dir-fmt': '{west_topdir}/build'}, None, west_topdir / 'build'),
129+
# must be able to resolve app
130+
({'dir-fmt': '{app}'}, None, 'app'),
131+
# must be able to resolve source_dir (when it is inside west workspace)
132+
(
133+
{'dir-fmt': '{source_dir}'},
134+
None,
135+
# source_dir resolves to relative path (relative to cwd), so build
136+
# directory is depending on cwd and ends up outside 'build'
137+
os.path.relpath(DEFAULT_TEST_ARGS.source_dir, TEST_CWD),
138+
),
139+
# source_dir dir is outside west workspace, so it is absolute path
140+
(
141+
{'dir-fmt': '{source_dir}'},
142+
Namespace(source_dir=ROOT / 'outside' / 'living' / 'app'),
143+
os.path.relpath(ROOT / 'outside' / 'living' / 'app', TEST_CWD),
144+
),
145+
# must be able to resolve source_dir_ws (when source_dir is inside west workspace)
146+
({'dir-fmt': '{source_dir_ws}'}, None, Path('subdir') / 'project' / 'app'),
147+
# must be able to resolve source_dir_ws (when source_dir is outside west workspace)
148+
(
149+
{'dir-fmt': 'build/{source_dir_ws}'},
150+
Namespace(source_dir=ROOT / 'outside' / 'living' / 'app'),
151+
Path('build') / 'outside' / 'living' / 'app',
152+
),
153+
# must be able to resolve board (must be specified)
154+
({'dir-fmt': '{board}'}, Namespace(board='native_sim'), 'native_sim'),
155+
]
156+
157+
158+
@pytest.mark.parametrize('test_case', TEST_CASES_BUILD_DIR)
159+
def test_dir_fmt(monkeypatch, test_case):
160+
# extract data from the test case
161+
config_build, test_args, expected = test_case
162+
163+
# apply given config_build
164+
config = configparser.ConfigParser()
165+
config.add_section("build")
166+
for k, v in config_build.items():
167+
config.set('build', k, v)
168+
monkeypatch.setattr("build_helpers.config", config)
169+
monkeypatch.setattr("build.config", config)
170+
171+
# set up and run _setup_build_dir
172+
b = setup_test_build(monkeypatch, test_args)
173+
b._setup_build_dir()
174+
175+
# check for expected build-dir
176+
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)