Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions doc/develop/west/build-flash-debug.rst
Original file line number Diff line number Diff line change
Expand Up @@ -395,10 +395,23 @@ You can :ref:`configure <west-config-cmd>` ``west build`` using these options.
west whenever it needs to create or locate a build folder. The currently
available arguments are:

- ``west_topdir``: The absolute path to the west workspace, as
returned by the ``west_topdir`` command
- ``board``: The board name
- ``source_dir``: The relative path from the current working directory
to the source directory. If the current working directory is inside
the source directory this will be set to an empty string.
- ``source_dir``: Path to the CMake source directory, relative to the
current working directory. If the current working directory is
inside the source directory, this is an empty string. If no source
directory is specified, it defaults to current working directory.
E.g. if ``west build ../app`` is run from ``<west_topdir>/app1``,
``source_dir`` resolves to ``../app`` (which is the relative path
to the current working dir).
- ``source_dir_workspace``: Path to the source directory, relative to
``west_topdir`` (if it is inside the workspace). Otherwise, it is
relative to the filesystem root (``/`` on Unix, respectively
``C:/`` on Windows).
E.g. if ``west build ../app`` is run from ``<west_topdir>/app1``,
``source_dir`` resolves to ``app`` (which is the relative path to
the west workspace dir).
- ``app``: The name of the source directory.
* - ``build.generator``
- String, default ``Ninja``. The `CMake Generator`_ to use to create a
Expand Down
37 changes: 29 additions & 8 deletions scripts/west_commands/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from build_helpers import FIND_BUILD_DIR_DESCRIPTION, find_build_dir, is_zephyr_build, load_domains
from west.commands import Verbosity
from west.configuration import config
from west.util import west_topdir
from west.util import WestNotFound, west_topdir
from west.version import __version__
from zcmake import DEFAULT_CMAKE_GENERATOR, CMakeCache, run_build, run_cmake
from zephyr_ext_common import Forceable
Expand Down Expand Up @@ -287,11 +287,11 @@ def _find_board(self):
if board is not None:
return (board, origin)

if self.args.board:
if getattr(self.args, 'board', None):
board, origin = self.args.board, 'command line'
elif 'BOARD' in os.environ:
board, origin = os.environ['BOARD'], 'env'
elif self.config_board is not None:
elif getattr(self, 'config_board', None) and self.config_board is not None:
board, origin = self.config_board, 'configfile'
return board, origin

Expand Down Expand Up @@ -456,16 +456,37 @@ def _update_cache(self):
with contextlib.suppress(FileNotFoundError):
self.cmake_cache = CMakeCache.from_build_dir(self.build_dir)

def _get_dir_fmt_context(self):
# Return a dictionary of build attributes which are used while
# substituting the placeholders in the build.dir-fmt format string.
source_dir = pathlib.Path(self._find_source_dir())
app = source_dir.name
board, _ = self._find_board()
try:
west_top_dir = west_topdir(source_dir)
except WestNotFound:
west_top_dir = pathlib.Path.cwd()
context = {
"west_topdir" : str(west_top_dir),
"source_dir" : str(source_dir),
"app" : app,
"board" : board
}
if source_dir.is_relative_to(west_top_dir):
context['source_dir_workspace'] = str(source_dir.relative_to(west_top_dir))
else:
context['source_dir_workspace'] = str(source_dir.relative_to(source_dir.anchor))
self.dbg(f'dir-fmt context: {context}', level=Verbosity.DBG_EXTREME)
return context

def _setup_build_dir(self):
# Initialize build_dir and created_build_dir attributes.
# If we created the build directory, we must run CMake.
self.dbg('setting up build directory', level=Verbosity.DBG_EXTREME)
# The CMake Cache has not been loaded yet, so this is safe
board, _ = self._find_board()
source_dir = self._find_source_dir()
app = os.path.split(source_dir)[1]
build_dir = find_build_dir(self.args.build_dir, board=board,
source_dir=source_dir, app=app)

context = self._get_dir_fmt_context()
build_dir = find_build_dir(self.args.build_dir, **context)
if not build_dir:
self.die('Unable to determine a default build folder. Check '
'your build.dir-fmt configuration option')
Expand Down
176 changes: 176 additions & 0 deletions scripts/west_commands/tests/west_build/test_dir_fmt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# Copyright (c) 2018 Nordic Semiconductor ASA
#
# SPDX-License-Identifier: Apache-2.0

import configparser
import copy
import os
from argparse import Namespace
from pathlib import Path

import pytest
from build import Build

ROOT = Path(Path.cwd().anchor)
TEST_CWD = ROOT / 'path' / 'to' / 'my' / 'current' / 'cwd'
TEST_CWD_RELATIVE_TO_ROOT = TEST_CWD.relative_to(ROOT)


def setup_test_build(monkeypatch, test_args=None):
# mock configparser read method to keep tests independent from actual configs
monkeypatch.setattr(configparser.ConfigParser, 'read', lambda self, filenames: None)
# mock os.makedirs to be independent from actual filesystem
monkeypatch.setattr('os.makedirs', lambda *a, **kw: None)
# mock west_topdir so that tests run independent from user machine
monkeypatch.setattr('build.west_topdir', lambda *a, **kw: str(west_topdir))
# mock os.getcwd and Path.cwd to use TEST_CWD
monkeypatch.setattr('os.getcwd', lambda *a, **kw: str(TEST_CWD))
monkeypatch.setattr('pathlib.Path.cwd', lambda *a, **kw: TEST_CWD)
# mock os.environ to ignore all environment variables during test
monkeypatch.setattr('os.environ', {})

# set up Build
b = Build()

# apply test args
b.args = copy.copy(DEFAULT_TEST_ARGS)
if test_args:
for k, v in vars(test_args).items():
setattr(b.args, k, v)

return b


# Use a hardcoded west_topdir to ensure that the tests run independently
# from the actual user machine
west_topdir = ROOT / 'any' / 'west' / 'workspace'

DEFAULT_TEST_ARGS = Namespace(
board=None, source_dir=west_topdir / 'subdir' / 'project' / 'app', build_dir=None
)

TEST_CASES_GET_DIR_FMT_CONTEXT = [
# (test_args, source_dir, expected)
# fallback to cwd in default case
(
{},
None,
{
'board': None,
'west_topdir': str(west_topdir),
'app': TEST_CWD.name,
'source_dir': str(TEST_CWD),
'source_dir_workspace': str(TEST_CWD_RELATIVE_TO_ROOT),
},
),
# check for correct source_dir and source_dir_workspace (if inside west_topdir)
(
{},
west_topdir / 'my' / 'project',
{
'board': None,
'west_topdir': str(west_topdir),
'app': 'project',
'source_dir': str(west_topdir / 'my' / 'project'),
'source_dir_workspace': str(Path('my') / 'project'),
},
),
# check for correct source_dir and source_dir_workspace (if outside west_topdir)
(
{},
ROOT / 'path' / 'to' / 'my-project',
{
'board': None,
'west_topdir': str(west_topdir),
'app': 'my-project',
'source_dir': str(ROOT / 'path' / 'to' / 'my-project'),
'source_dir_workspace': str(Path('path') / 'to' / 'my-project'),
},
),
# check for correct board
(
Namespace(board='native_sim'),
None,
{
'board': 'native_sim',
'west_topdir': str(west_topdir),
'app': TEST_CWD.name,
'source_dir': str(TEST_CWD),
'source_dir_workspace': str(TEST_CWD_RELATIVE_TO_ROOT),
},
),
]


@pytest.mark.parametrize('test_case', TEST_CASES_GET_DIR_FMT_CONTEXT)
def test_get_dir_fmt_context(monkeypatch, test_case):
# extract data from the test case
test_args, source_dir, expected = test_case

# set up and run _get_dir_fmt_context
b = setup_test_build(monkeypatch, test_args)
b.args.source_dir = source_dir
actual = b._get_dir_fmt_context()
assert expected == actual


TEST_CASES_BUILD_DIR = [
# (config_build, test_args, expected)
# default build directory if no args and dir-fmt are specified
({}, None, Path('build')),
# build_dir from args should always be preferred (if it is specified)
({}, Namespace(build_dir='from-args'), Path('from-args')),
({'dir-fmt': 'from-dir-fmt'}, Namespace(build_dir='from-args'), Path('from-args')),
# build_dir is determined by resolving dir-fmt format string
# must be able to resolve a simple string
({'dir-fmt': 'from-dir-fmt'}, None, 'from-dir-fmt'),
# must be able to resolve west_topdir
({'dir-fmt': '{west_topdir}/build'}, None, west_topdir / 'build'),
# must be able to resolve app
({'dir-fmt': '{app}'}, None, 'app'),
# must be able to resolve source_dir (when it is inside west workspace)
(
{'dir-fmt': '{source_dir}'},
None,
# source_dir resolves to relative path (relative to cwd), so build
# directory is depending on cwd and ends up outside 'build'
os.path.relpath(DEFAULT_TEST_ARGS.source_dir, TEST_CWD),
),
# source_dir dir is outside west workspace, so it is absolute path
(
{'dir-fmt': '{source_dir}'},
Namespace(source_dir=ROOT / 'outside' / 'living' / 'app'),
os.path.relpath(ROOT / 'outside' / 'living' / 'app', TEST_CWD),
),
# must be able to resolve source_dir_workspace (when source_dir is inside west workspace)
({'dir-fmt': '{source_dir_workspace}'}, None, Path('subdir') / 'project' / 'app'),
# must be able to resolve source_dir_workspace (when source_dir is outside west workspace)
(
{'dir-fmt': 'build/{source_dir_workspace}'},
Namespace(source_dir=ROOT / 'outside' / 'living' / 'app'),
Path('build') / 'outside' / 'living' / 'app',
),
# must be able to resolve board (must be specified)
({'dir-fmt': '{board}'}, Namespace(board='native_sim'), 'native_sim'),
]


@pytest.mark.parametrize('test_case', TEST_CASES_BUILD_DIR)
def test_dir_fmt(monkeypatch, test_case):
# extract data from the test case
config_build, test_args, expected = test_case

# apply given config_build
config = configparser.ConfigParser()
config.add_section("build")
for k, v in config_build.items():
config.set('build', k, v)
monkeypatch.setattr("build_helpers.config", config)
monkeypatch.setattr("build.config", config)

# set up and run _setup_build_dir
b = setup_test_build(monkeypatch, test_args)
b._setup_build_dir()

# check for expected build-dir
assert os.path.abspath(expected) == b.build_dir
40 changes: 40 additions & 0 deletions scripts/west_commands/tests/west_build/test_resolve_build_dir.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Copyright (c) 2018 Nordic Semiconductor ASA
#
# SPDX-License-Identifier: Apache-2.0

from pathlib import Path

import pytest
from build_helpers import _resolve_build_dir

cwd = Path.cwd()
root = Path(cwd.anchor)
TEST_CWD = root / 'path' / 'to' / 'my' / 'current' / 'cwd'

TEST_CASES_RESOLVE_BUILD_DIR = [
# (fmt, kwargs, expected)
# simple string (no format string)
('simple/string', {}, 'simple/string'),
# source_dir is inside cwd
('{source_dir}', {'source_dir': TEST_CWD / 'subdir'}, 'subdir'),
# source_dir is outside cwd
('{source_dir}', {'source_dir': TEST_CWD / '..' / 'subdir'}, str(Path('..') / 'subdir')),
# cwd is inside source dir
('{source_dir}', {'source_dir': TEST_CWD / '..'}, ''),
# source dir not resolvable by default
('{source_dir}', {}, None),
# invalid format arg
('{invalid}', {}, None),
# app is defined by default
('{app}', {}, None),
]


@pytest.mark.parametrize('test_case', TEST_CASES_RESOLVE_BUILD_DIR)
def test_resolve_build_dir(test_case):
fmt, kwargs, expected = test_case

# test both guess=True and guess=False
for guess in [True, False]:
actual = _resolve_build_dir(cwd=TEST_CWD, guess=guess, fmt=fmt, **kwargs)
assert actual == expected
Loading