spl-west-deps 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,155 @@
1
+ Metadata-Version: 2.4
2
+ Name: spl-west-deps
3
+ Version: 0.1.0
4
+ Summary: An spl-core build extension that installs the west dependencies of a variant
5
+ License-Expression: MIT
6
+ Author: cuinixam
7
+ Author-email: cuinixam@gmail.com
8
+ Requires-Python: <4.0,>=3.10
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Natural Language :: English
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Build Tools
18
+ Requires-Dist: py-app-dev (>=2.1,<3)
19
+ Requires-Dist: pypeline-runner (>=1.36,<2)
20
+ Requires-Dist: typer (>=0,<1)
21
+ Description-Content-Type: text/markdown
22
+
23
+ # spl-west-deps
24
+
25
+ An spl-core build extension that installs the [west](https://docs.zephyrproject.org/latest/develop/west/index.html)
26
+ dependencies of a variant.
27
+
28
+ The dependencies of an SPL are declared in west manifests: one at the project
29
+ root for what every variant needs, and one per variant for what that variant
30
+ adds or pins differently. The extension resolves both, installs them, and tells
31
+ CMake where they landed, so a variant can consume a dependency without any
32
+ consumer hard-coding the workspace layout.
33
+
34
+ ![maintained](https://img.shields.io/badge/maintained-yes-success?style=flat-square)
35
+ ![ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)
36
+ ![pypeline](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/cuinixam/pypeline/main/assets/badge/v0.json)
37
+
38
+ The install itself is done by [pypeline](https://github.com/cuinixam/pypeline)'s
39
+ `WestInstall` step, which already merges several manifests, installs them and
40
+ reports where each dependency landed. This extension builds the execution context
41
+ that step reads, and turns its result into CMake variables.
42
+
43
+ ## Installation
44
+
45
+ Install this via pip (or your favorite package manager):
46
+
47
+ `pip install spl_west_deps`
48
+
49
+ Then, in the consuming spl-core project's `CMakeLists.txt`, after `spl.cmake` and
50
+ **before** the variant parts are included:
51
+
52
+ ```cmake
53
+ include(".venv/Lib/site-packages/spl_west_deps/index.cmake")
54
+
55
+ include(${PROJECT_SOURCE_DIR}/variants/${VARIANT}/parts.cmake)
56
+ ```
57
+
58
+ The order matters: `parts.cmake` calls `spl_add_component()` on components whose
59
+ sources come from the dependencies, so the dependencies must be installed first.
60
+
61
+ Remove any west step from the project's own pipeline when you enable this
62
+ extension. Two resolvers writing one workspace fight over the checkouts.
63
+
64
+ ## Usage
65
+
66
+ The extension needs no CMake target and no command of its own. It runs during the
67
+ CMake configuration and defines one variable per dependency:
68
+
69
+ | Dependency name in the manifest | CMake variable |
70
+ | --- | --- |
71
+ | `brightness-controller` | `MODULE_BRIGHTNESS_CONTROLLER_PATH` |
72
+
73
+ A manifest project name is not a legal CMake identifier, so every character which
74
+ is not a letter or a digit becomes an underscore. A component consumes the
75
+ dependency through the variable and hardcodes no path:
76
+
77
+ ```cmake
78
+ spl_add_source(${MODULE_BRIGHTNESS_CONTROLLER_PATH}/export/brightness_controller.c)
79
+ ```
80
+
81
+ ## Manifests
82
+
83
+ | File | Scope |
84
+ | --- | --- |
85
+ | `west.yaml` | Every variant |
86
+ | `variants/<VARIANT>/west.yaml` | That variant. Overrides the root manifest by project name |
87
+
88
+ An overlay can add a dependency and override one, but it cannot remove one. That
89
+ follows from merging by project name. So keep the root manifest to what every
90
+ variant needs.
91
+
92
+ Every dependency is installed under its revision, so two variants pinning one
93
+ dependency at two revisions get two checkouts instead of fighting over one:
94
+
95
+ ```
96
+ build/modules/brightness-controller/generated-code-v0.1.0/
97
+ build/modules/brightness-controller/generated-code-v0.5.0/
98
+ ```
99
+
100
+ ## How it works
101
+
102
+ Unlike the other spl-core extensions, `index.cmake` does **not**
103
+ `cmake_language(DEFER ...)` its work to the end of the CMake configuration: the
104
+ dependency sources have to exist before the variant parts are processed. So the
105
+ file runs `spl_west_deps generate ...` while it is being included, then
106
+ `include()`s the generated `${CMAKE_BINARY_DIR}/spl_west_deps.cmake`.
107
+
108
+ The `generate` command:
109
+
110
+ 1. Creates a pypeline `ExecutionContext` for the project.
111
+ 2. Registers `variants/<VARIANT>/west.yaml` in the context's data registry, when
112
+ the variant has one. The `WestInstall` step merges the registered manifests
113
+ after the project root manifest, which is what makes the variant override it.
114
+ 3. Instantiates `WestInstall` with `revision_scoped_paths` and runs it through
115
+ py-app-dev's `Executor`. The step is skipped while the manifests and the
116
+ installed directories are unchanged, so a repeated CMake configuration costs
117
+ nothing.
118
+ 4. Reads the `ExternalProject` entries the step publishes and writes one
119
+ `set(MODULE_<NAME>_PATH ...)` per dependency.
120
+
121
+ Step 4 uses the step's `update_execution_context()`, which reads the step's
122
+ result file rather than its in-memory state. That is why the variables are
123
+ complete even on a run where the install was skipped.
124
+
125
+ ## Start developing
126
+
127
+ The project is managed with [uv](https://docs.astral.sh/uv/) and orchestrated by
128
+ [pypeline](https://github.com/cuinixam/pypeline). Bootstrap the environment and
129
+ run the full pipeline (venv, pre-commit, tests) with:
130
+
131
+ ```shell
132
+ pypeline run
133
+ ```
134
+
135
+ ## Committing changes
136
+
137
+ This repository uses [commitlint](https://github.com/conventional-changelog/commitlint) for checking if the commit message meets the [conventional commit format](https://www.conventionalcommits.org/en). Commit messages drive the automated release.
138
+
139
+ ## Continuous integration & release
140
+
141
+ CI runs on **GitHub Actions** (`.github/workflows/ci.yml`): lint (pre-commit),
142
+ commitlint, a test matrix (Python 3.10/3.13 × Ubuntu/Windows), and a release job.
143
+ Releases are automated with [python-semantic-release](https://python-semantic-release.readthedocs.io/):
144
+ merging to `main` bumps the version from the conventional-commit history and
145
+ publishes to PyPI (trusted publishing) and GitHub Releases.
146
+
147
+ ## Credits
148
+
149
+ [![Copier](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/copier-org/copier/master/img/badge/badge-grayscale-inverted-border-orange.json)](https://github.com/copier-org/copier)
150
+
151
+ This package was created with
152
+ [Copier](https://copier.readthedocs.io/) and the
153
+ [browniebroke/pypackage-template](https://github.com/browniebroke/pypackage-template)
154
+ project template.
155
+
@@ -0,0 +1,132 @@
1
+ # spl-west-deps
2
+
3
+ An spl-core build extension that installs the [west](https://docs.zephyrproject.org/latest/develop/west/index.html)
4
+ dependencies of a variant.
5
+
6
+ The dependencies of an SPL are declared in west manifests: one at the project
7
+ root for what every variant needs, and one per variant for what that variant
8
+ adds or pins differently. The extension resolves both, installs them, and tells
9
+ CMake where they landed, so a variant can consume a dependency without any
10
+ consumer hard-coding the workspace layout.
11
+
12
+ ![maintained](https://img.shields.io/badge/maintained-yes-success?style=flat-square)
13
+ ![ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)
14
+ ![pypeline](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/cuinixam/pypeline/main/assets/badge/v0.json)
15
+
16
+ The install itself is done by [pypeline](https://github.com/cuinixam/pypeline)'s
17
+ `WestInstall` step, which already merges several manifests, installs them and
18
+ reports where each dependency landed. This extension builds the execution context
19
+ that step reads, and turns its result into CMake variables.
20
+
21
+ ## Installation
22
+
23
+ Install this via pip (or your favorite package manager):
24
+
25
+ `pip install spl_west_deps`
26
+
27
+ Then, in the consuming spl-core project's `CMakeLists.txt`, after `spl.cmake` and
28
+ **before** the variant parts are included:
29
+
30
+ ```cmake
31
+ include(".venv/Lib/site-packages/spl_west_deps/index.cmake")
32
+
33
+ include(${PROJECT_SOURCE_DIR}/variants/${VARIANT}/parts.cmake)
34
+ ```
35
+
36
+ The order matters: `parts.cmake` calls `spl_add_component()` on components whose
37
+ sources come from the dependencies, so the dependencies must be installed first.
38
+
39
+ Remove any west step from the project's own pipeline when you enable this
40
+ extension. Two resolvers writing one workspace fight over the checkouts.
41
+
42
+ ## Usage
43
+
44
+ The extension needs no CMake target and no command of its own. It runs during the
45
+ CMake configuration and defines one variable per dependency:
46
+
47
+ | Dependency name in the manifest | CMake variable |
48
+ | --- | --- |
49
+ | `brightness-controller` | `MODULE_BRIGHTNESS_CONTROLLER_PATH` |
50
+
51
+ A manifest project name is not a legal CMake identifier, so every character which
52
+ is not a letter or a digit becomes an underscore. A component consumes the
53
+ dependency through the variable and hardcodes no path:
54
+
55
+ ```cmake
56
+ spl_add_source(${MODULE_BRIGHTNESS_CONTROLLER_PATH}/export/brightness_controller.c)
57
+ ```
58
+
59
+ ## Manifests
60
+
61
+ | File | Scope |
62
+ | --- | --- |
63
+ | `west.yaml` | Every variant |
64
+ | `variants/<VARIANT>/west.yaml` | That variant. Overrides the root manifest by project name |
65
+
66
+ An overlay can add a dependency and override one, but it cannot remove one. That
67
+ follows from merging by project name. So keep the root manifest to what every
68
+ variant needs.
69
+
70
+ Every dependency is installed under its revision, so two variants pinning one
71
+ dependency at two revisions get two checkouts instead of fighting over one:
72
+
73
+ ```
74
+ build/modules/brightness-controller/generated-code-v0.1.0/
75
+ build/modules/brightness-controller/generated-code-v0.5.0/
76
+ ```
77
+
78
+ ## How it works
79
+
80
+ Unlike the other spl-core extensions, `index.cmake` does **not**
81
+ `cmake_language(DEFER ...)` its work to the end of the CMake configuration: the
82
+ dependency sources have to exist before the variant parts are processed. So the
83
+ file runs `spl_west_deps generate ...` while it is being included, then
84
+ `include()`s the generated `${CMAKE_BINARY_DIR}/spl_west_deps.cmake`.
85
+
86
+ The `generate` command:
87
+
88
+ 1. Creates a pypeline `ExecutionContext` for the project.
89
+ 2. Registers `variants/<VARIANT>/west.yaml` in the context's data registry, when
90
+ the variant has one. The `WestInstall` step merges the registered manifests
91
+ after the project root manifest, which is what makes the variant override it.
92
+ 3. Instantiates `WestInstall` with `revision_scoped_paths` and runs it through
93
+ py-app-dev's `Executor`. The step is skipped while the manifests and the
94
+ installed directories are unchanged, so a repeated CMake configuration costs
95
+ nothing.
96
+ 4. Reads the `ExternalProject` entries the step publishes and writes one
97
+ `set(MODULE_<NAME>_PATH ...)` per dependency.
98
+
99
+ Step 4 uses the step's `update_execution_context()`, which reads the step's
100
+ result file rather than its in-memory state. That is why the variables are
101
+ complete even on a run where the install was skipped.
102
+
103
+ ## Start developing
104
+
105
+ The project is managed with [uv](https://docs.astral.sh/uv/) and orchestrated by
106
+ [pypeline](https://github.com/cuinixam/pypeline). Bootstrap the environment and
107
+ run the full pipeline (venv, pre-commit, tests) with:
108
+
109
+ ```shell
110
+ pypeline run
111
+ ```
112
+
113
+ ## Committing changes
114
+
115
+ This repository uses [commitlint](https://github.com/conventional-changelog/commitlint) for checking if the commit message meets the [conventional commit format](https://www.conventionalcommits.org/en). Commit messages drive the automated release.
116
+
117
+ ## Continuous integration & release
118
+
119
+ CI runs on **GitHub Actions** (`.github/workflows/ci.yml`): lint (pre-commit),
120
+ commitlint, a test matrix (Python 3.10/3.13 × Ubuntu/Windows), and a release job.
121
+ Releases are automated with [python-semantic-release](https://python-semantic-release.readthedocs.io/):
122
+ merging to `main` bumps the version from the conventional-commit history and
123
+ publishes to PyPI (trusted publishing) and GitHub Releases.
124
+
125
+ ## Credits
126
+
127
+ [![Copier](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/copier-org/copier/master/img/badge/badge-grayscale-inverted-border-orange.json)](https://github.com/copier-org/copier)
128
+
129
+ This package was created with
130
+ [Copier](https://copier.readthedocs.io/) and the
131
+ [browniebroke/pypackage-template](https://github.com/browniebroke/pypackage-template)
132
+ project template.
@@ -0,0 +1,148 @@
1
+ [project]
2
+ name = "spl-west-deps"
3
+ version = "0.1.0"
4
+ description = "An spl-core build extension that installs the west dependencies of a variant"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [
8
+ { name = "cuinixam", email = "cuinixam@gmail.com" },
9
+ ]
10
+ requires-python = "<4.0,>=3.10"
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Intended Audience :: Developers",
14
+ "Natural Language :: English",
15
+ "Operating System :: OS Independent",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Topic :: Software Development :: Build Tools",
21
+ ]
22
+
23
+ dependencies = [
24
+ "typer>=0,<1",
25
+ "py-app-dev>=2.1,<3",
26
+ # The WestInstall step, which owns the manifest merge and the west calls.
27
+ # 1.36 is the first version with revision-scoped install paths.
28
+ "pypeline-runner>=1.36,<2",
29
+ ]
30
+
31
+ [project.scripts]
32
+ spl_west_deps = "spl_west_deps.main:app"
33
+
34
+ [dependency-groups]
35
+ dev = [
36
+ "coverage>=7,<8",
37
+ "pytest>=9,<10",
38
+ "pytest-cov>=7,<8",
39
+ "pypeline-runner>=1,<2",
40
+ "pre-commit>=4,<5",
41
+ "ruff>=0,<1",
42
+ ]
43
+
44
+ [build-system]
45
+ requires = ["poetry-core>=2,<3"]
46
+ build-backend = "poetry.core.masonry.api"
47
+
48
+ [tool.poetry]
49
+ packages = [{ include = "spl_west_deps", from = "src" }]
50
+
51
+ [tool.ruff]
52
+ target-version = "py310"
53
+ line-length = 180
54
+ lint.select = [
55
+ "B", # flake8-bugbear
56
+ "C4", # flake8-comprehensions
57
+ "S", # flake8-bandit
58
+ "F", # pyflake
59
+ "E", # pycodestyle
60
+ "W", # pycodestyle
61
+ "UP", # pyupgrade
62
+ "I", # isort
63
+ "RUF", # ruff specific
64
+ ]
65
+ lint.ignore = [
66
+ "D203",
67
+ "D212",
68
+ "D100",
69
+ "D101",
70
+ "D102",
71
+ "D103",
72
+ "D104",
73
+ "D107",
74
+ "D401",
75
+ ]
76
+ lint.per-file-ignores."bootstrap.py" = ["UP006", "UP035", "S603", "D200", "D205", "D417"]
77
+ lint.per-file-ignores."conftest.py" = ["D100"]
78
+ lint.per-file-ignores."setup.py" = ["D100"]
79
+ lint.per-file-ignores."tests/**/*" = ["D100", "D101", "D102", "D103", "D104", "S101"]
80
+ lint.isort.known-first-party = ["spl_west_deps", "tests"]
81
+
82
+ [tool.pytest.ini_options]
83
+ addopts = "-vv -Wdefault --cov=spl_west_deps --cov-report=term-missing:skip-covered --cov-report=xml -s"
84
+ pythonpath = ["src"]
85
+ testpaths = ["tests"]
86
+
87
+ [tool.coverage.run]
88
+ branch = true
89
+
90
+ [tool.coverage.report]
91
+ exclude_lines = [
92
+ "pragma: no cover",
93
+ "@overload",
94
+ "if TYPE_CHECKING",
95
+ "raise NotImplementedError",
96
+ 'if __name__ == "__main__":',
97
+ ]
98
+
99
+ [tool.mypy]
100
+ check_untyped_defs = true
101
+ disallow_any_generics = true
102
+ disallow_incomplete_defs = true
103
+ disallow_untyped_defs = true
104
+ mypy_path = "src/"
105
+ no_implicit_optional = true
106
+ show_error_codes = true
107
+ warn_unreachable = true
108
+ warn_unused_ignores = true
109
+ exclude = ['setup.py']
110
+ disable_error_code = ["import-untyped"]
111
+
112
+ [[tool.mypy.overrides]]
113
+ module = "tests.*"
114
+ allow_untyped_defs = true
115
+
116
+ [tool.semantic_release]
117
+ allow_zero_version = true
118
+ version_toml = ["pyproject.toml:project.version"]
119
+ version_variables = [
120
+ "src/spl_west_deps/__init__.py:__version__",
121
+ ]
122
+ build_command = """
123
+ pip install uv
124
+ uv lock
125
+ git add uv.lock
126
+ uv build
127
+ """
128
+
129
+ [tool.semantic_release.changelog]
130
+ exclude_commit_patterns = [
131
+ '''chore(?:\([^)]*?\))?: .+''',
132
+ '''ci(?:\([^)]*?\))?: .+''',
133
+ '''refactor(?:\([^)]*?\))?: .+''',
134
+ '''style(?:\([^)]*?\))?: .+''',
135
+ '''test(?:\([^)]*?\))?: .+''',
136
+ '''build\((?!deps\): .+)''',
137
+ '''Merged? .*''',
138
+ ]
139
+
140
+ [tool.semantic_release.changelog.environment]
141
+ keep_trailing_newline = true
142
+
143
+ [tool.semantic_release.branches.main]
144
+ match = "main"
145
+
146
+ [tool.semantic_release.branches.noop]
147
+ match = "(?!main$)"
148
+ prerelease = true
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0" # Update as needed
@@ -0,0 +1,29 @@
1
+ # There is no cmake_language(DEFER ...) here, unlike the other spl-core extensions.
2
+ # The dependency sources must exist before variants/${VARIANT}/parts.cmake calls
3
+ # spl_add_component(), so this runs while it is being included, not at the end of
4
+ # the CMake configuration. Include it before the variant parts.
5
+
6
+ set(WEST_DEPS_RUNNER spl_west_deps)
7
+ set(WEST_DEPS_CMAKE_FILE ${CMAKE_BINARY_DIR}/spl_west_deps.cmake)
8
+
9
+ execute_process(
10
+ COMMAND ${WEST_DEPS_RUNNER} generate --project-root-dir ${CMAKE_SOURCE_DIR} --variant ${VARIANT} --output-file ${WEST_DEPS_CMAKE_FILE}
11
+ WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
12
+ OUTPUT_VARIABLE output
13
+ ERROR_VARIABLE error
14
+ RESULT_VARIABLE result
15
+ )
16
+
17
+ # print stdout/stderr
18
+ message(STATUS "STDERR: ${error}")
19
+ message(STATUS "STDOUT: ${output}")
20
+
21
+ # check if the command succeeded
22
+ if(result)
23
+ message(FATAL_ERROR "Installation of the West Dependencies extension failed.")
24
+ else()
25
+ message(STATUS "Installation of the West Dependencies extension was successful.")
26
+ endif()
27
+
28
+ # include the generated cmake file
29
+ include(${WEST_DEPS_CMAKE_FILE})
@@ -0,0 +1,42 @@
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ import typer
5
+ from py_app_dev.core.logging import logger
6
+
7
+ from spl_west_deps.west_installer import install_dependencies
8
+
9
+ package_name = "spl_west_deps"
10
+
11
+ app = typer.Typer(name=package_name, help="An spl-core extension to install the west dependencies of a variant", add_completion=False)
12
+
13
+
14
+ @app.callback(invoke_without_command=True)
15
+ def version(version: bool = typer.Option(None, "--version", "-v", is_eager=True, help="Show version and exit.")) -> None:
16
+ """Print package version and exit when --version supplied."""
17
+ if version:
18
+ from spl_west_deps import __version__
19
+
20
+ typer.echo(__version__)
21
+ raise typer.Exit()
22
+
23
+
24
+ @app.command()
25
+ def generate(
26
+ project_root_dir: Path = typer.Option(..., "--project-root-dir", help="The project root directory."), # noqa: B008
27
+ variant: str = typer.Option(..., "--variant", help="The variant of the project."),
28
+ output_file: Path = typer.Option(..., "--output-file", help="The CMake file to generate."), # noqa: B008
29
+ ) -> None:
30
+ """Install the west dependencies of this variant and generate the CMake file."""
31
+ logger.info("[CLI] generate command invoked")
32
+ try:
33
+ install_dependencies(project_root_dir, variant, output_file)
34
+ logger.info("[CLI] generate command completed successfully")
35
+ except Exception as e:
36
+ logger.error(f"[CLI] generate command failed: {e}")
37
+ typer.echo(f"Error installing west dependencies: {e}", err=True)
38
+ sys.exit(1)
39
+
40
+
41
+ if __name__ == "__main__":
42
+ app()
File without changes
@@ -0,0 +1,94 @@
1
+ """Install the west dependencies of a variant and publish their locations to CMake.
2
+
3
+ The install itself is done by pypeline's ``WestInstall`` step, which already
4
+ merges several manifests, installs them and reports where each dependency
5
+ landed. This module only builds the execution context that step reads, and
6
+ turns its result into CMake variables.
7
+ """
8
+
9
+ import re
10
+ from pathlib import Path
11
+
12
+ from py_app_dev.core.exceptions import UserNotificationException
13
+ from py_app_dev.core.logging import logger
14
+ from py_app_dev.core.runnable import Executor
15
+ from pypeline.domain.execution_context import ExecutionContext
16
+ from pypeline.domain.external_project import ExternalProject
17
+ from pypeline.steps.west_install import WestInstall, WestManifestFile
18
+
19
+ #: Manifest file name, both at the project root and inside a variant directory.
20
+ MANIFEST_FILE_NAME = "west.yaml"
21
+ #: Directory holding the variant directories, relative to the project root.
22
+ VARIANTS_DIR_NAME = "variants"
23
+ #: Workspace the dependencies are installed into, relative to the project root.
24
+ #: Every variant shares it. Two variants pinning one dependency at two revisions
25
+ #: do not collide, because the revision is part of each install path.
26
+ WORKSPACE_DIR = "build/modules"
27
+ #: Name this package registers its data under, shown as the provider in the registry.
28
+ PROVIDER_NAME = "spl_west_deps"
29
+
30
+
31
+ def install_dependencies(project_root_dir: Path, variant: str, output_file: Path) -> None:
32
+ """Install the west dependencies of the variant and write the CMake file."""
33
+ logger.info(f"Installing the west dependencies of variant {variant}")
34
+ execution_context = create_execution_context(project_root_dir, variant)
35
+
36
+ # The step collects its manifests in __init__, so the variant manifest must
37
+ # already be in the data registry by now.
38
+ step = WestInstall(
39
+ execution_context,
40
+ group_name=variant,
41
+ config={"workspace_dir": WORKSPACE_DIR, "revision_scoped_paths": True},
42
+ )
43
+
44
+ # The Executor skips the west calls while the manifests and the installed
45
+ # directories are unchanged. Without it every CMake configuration would
46
+ # fetch again, and CMake configures often.
47
+ exit_code = Executor(cache_dir=step.output_dir).execute(step)
48
+ if exit_code:
49
+ raise UserNotificationException(f"The WestInstall step failed with exit code {exit_code}.")
50
+
51
+ # Publishes one ExternalProject per dependency. It reads the step's result
52
+ # file, so the list is complete even when the step itself was skipped.
53
+ step.update_execution_context()
54
+
55
+ write_cmake_file(output_file, execution_context.data_registry.find_data(ExternalProject))
56
+
57
+
58
+ def create_execution_context(project_root_dir: Path, variant: str) -> ExecutionContext:
59
+ """Create the execution context the WestInstall step runs in."""
60
+ execution_context = ExecutionContext(project_root_dir=project_root_dir)
61
+
62
+ variant_manifest = project_root_dir / VARIANTS_DIR_NAME / variant / MANIFEST_FILE_NAME
63
+ if variant_manifest.is_file():
64
+ logger.info(f"Found variant west manifest: {variant_manifest}")
65
+ # The step merges the project root manifest first and the registered
66
+ # manifests after it, so this one overrides the root by project name.
67
+ execution_context.data_registry.insert(WestManifestFile.from_file(variant_manifest), provider=PROVIDER_NAME)
68
+ else:
69
+ logger.info(f"Variant {variant} has no {MANIFEST_FILE_NAME}. Only the project root manifest is used.")
70
+
71
+ return execution_context
72
+
73
+
74
+ def cmake_variable_name(project_name: str) -> str:
75
+ """Return the CMake variable holding a dependency's install path.
76
+
77
+ A manifest project name is not a legal CMake identifier (``-`` is the common
78
+ case), so every character which is not a letter or a digit becomes an
79
+ underscore: ``brightness-controller`` -> ``MODULE_BRIGHTNESS_CONTROLLER_PATH``.
80
+ """
81
+ return f"MODULE_{re.sub(r'[^A-Z0-9]+', '_', project_name.upper())}_PATH"
82
+
83
+
84
+ def write_cmake_file(output_file: Path, projects: list[ExternalProject]) -> None:
85
+ """Write the CMake file that ``index.cmake`` includes."""
86
+ lines = ["# West Dependencies Extension CMake Variables"]
87
+ for project in projects:
88
+ variable = cmake_variable_name(project.name)
89
+ logger.info(f"{variable} = {project.path}")
90
+ lines.append(f"set({variable} {project.path.as_posix()})")
91
+
92
+ output_file.parent.mkdir(parents=True, exist_ok=True)
93
+ output_file.write_text("\n".join(lines) + "\n")
94
+ logger.info(f"Generated CMake file at {output_file}")