embedagents-stm32 0.3.0__py3-none-any.whl

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.
Files changed (65) hide show
  1. embedagents/stm32/__init__.py +15 -0
  2. embedagents/stm32/_jsonc.py +141 -0
  3. embedagents/stm32/cli/__init__.py +125 -0
  4. embedagents/stm32/cli/_build.py +300 -0
  5. embedagents/stm32/cli/_debug.py +525 -0
  6. embedagents/stm32/cli/_mx.py +102 -0
  7. embedagents/stm32/cli/_prog.py +664 -0
  8. embedagents/stm32/cli/_serialize.py +108 -0
  9. embedagents/stm32/cli/_vcp.py +193 -0
  10. embedagents/stm32/context.py +647 -0
  11. embedagents/stm32/cubeide/__init__.py +44 -0
  12. embedagents/stm32/cubeide/client.py +1009 -0
  13. embedagents/stm32/cubeide/cproject.py +634 -0
  14. embedagents/stm32/cubeide/headless.py +161 -0
  15. embedagents/stm32/cubeide/presets.py +124 -0
  16. embedagents/stm32/cubeide/results.py +106 -0
  17. embedagents/stm32/cubeide/workspace.py +220 -0
  18. embedagents/stm32/cubemx/__init__.py +22 -0
  19. embedagents/stm32/cubemx/client.py +253 -0
  20. embedagents/stm32/cubemx/launcher.py +60 -0
  21. embedagents/stm32/cubemx/results.py +51 -0
  22. embedagents/stm32/cubemx/runner.py +350 -0
  23. embedagents/stm32/cubeprogrammer/__init__.py +65 -0
  24. embedagents/stm32/cubeprogrammer/client.py +1629 -0
  25. embedagents/stm32/cubeprogrammer/codes.py +68 -0
  26. embedagents/stm32/cubeprogrammer/diagnose.py +156 -0
  27. embedagents/stm32/cubeprogrammer/external_loader.py +84 -0
  28. embedagents/stm32/cubeprogrammer/parsers.py +897 -0
  29. embedagents/stm32/cubeprogrammer/results.py +293 -0
  30. embedagents/stm32/debug/__init__.py +72 -0
  31. embedagents/stm32/debug/client.py +320 -0
  32. embedagents/stm32/debug/gdb.py +432 -0
  33. embedagents/stm32/debug/gdbserver.py +355 -0
  34. embedagents/stm32/debug/parsers.py +571 -0
  35. embedagents/stm32/debug/pipereader.py +76 -0
  36. embedagents/stm32/debug/results.py +254 -0
  37. embedagents/stm32/debug/session.py +641 -0
  38. embedagents/stm32/debug/svd.py +670 -0
  39. embedagents/stm32/errors.py +304 -0
  40. embedagents/stm32/logging_setup.py +38 -0
  41. embedagents/stm32/platform/__init__.py +43 -0
  42. embedagents/stm32/platform/locking.py +172 -0
  43. embedagents/stm32/platform/process.py +254 -0
  44. embedagents/stm32/progress.py +34 -0
  45. embedagents/stm32/py.typed +0 -0
  46. embedagents/stm32/resolution.py +79 -0
  47. embedagents/stm32/schemas/__init__.py +0 -0
  48. embedagents/stm32/schemas/stm32-project.schema.json +189 -0
  49. embedagents/stm32/schemas/stm32-runtime-defaults.schema.json +264 -0
  50. embedagents/stm32/schemas/stm32-tools.local.schema.json +114 -0
  51. embedagents/stm32/signing/__init__.py +23 -0
  52. embedagents/stm32/signing/client.py +302 -0
  53. embedagents/stm32/signing/results.py +33 -0
  54. embedagents/stm32/subprocess_runner.py +229 -0
  55. embedagents/stm32/vcp/__init__.py +40 -0
  56. embedagents/stm32/vcp/client.py +451 -0
  57. embedagents/stm32/vcp/discovery.py +84 -0
  58. embedagents/stm32/vcp/reader.py +483 -0
  59. embedagents/stm32/vcp/results.py +84 -0
  60. embedagents_stm32-0.3.0.dist-info/METADATA +315 -0
  61. embedagents_stm32-0.3.0.dist-info/RECORD +65 -0
  62. embedagents_stm32-0.3.0.dist-info/WHEEL +5 -0
  63. embedagents_stm32-0.3.0.dist-info/entry_points.txt +2 -0
  64. embedagents_stm32-0.3.0.dist-info/licenses/LICENSE +21 -0
  65. embedagents_stm32-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,15 @@
1
+ """STM32 substrate — deterministic Python wrapper around ST's vendor CLIs.
2
+
3
+ ``__version__`` is derived from the installed package metadata so it can never
4
+ drift from ``pyproject.toml``. In a bare source checkout (package not installed)
5
+ it falls back to a sentinel.
6
+ """
7
+
8
+ from importlib.metadata import PackageNotFoundError, version
9
+
10
+ try:
11
+ __version__ = version("embedagents-stm32")
12
+ except PackageNotFoundError: # source checkout, not installed
13
+ __version__ = "0.0.0+unknown"
14
+
15
+ __all__ = ["__version__"]
@@ -0,0 +1,141 @@
1
+ """Minimal JSONC loader (strip ``//`` + ``/* ... */`` comments, accept trailing commas).
2
+
3
+ Substrate uses JSONC for human-edited config files per SC-005. The stdlib
4
+ ``json`` module does not accept comments, so we strip them in a single pass
5
+ that is aware of string literals (a ``"// not a comment"`` string is
6
+ preserved untouched).
7
+
8
+ Public surface:
9
+ ``load_jsonc(text)`` — parse a JSONC string and return the value.
10
+ ``load_jsonc_file(path)`` — read + parse a JSONC file.
11
+
12
+ The stripper handles the common cases used by ST-tooling configs:
13
+
14
+ - ``//`` line comments
15
+ - ``/* ... */`` block comments (single-line and multi-line)
16
+ - trailing commas immediately before ``}`` or ``]``
17
+ - string escapes (``\\"``) inside strings
18
+
19
+ It is intentionally not a full JSON5 parser. For v1, simpler is better
20
+ (M-018). If users want JSON5 features (unquoted keys, single quotes,
21
+ hex numbers), the substrate raises a clean parse error pointing at the
22
+ offending position.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import json
28
+ from pathlib import Path
29
+ from typing import Any
30
+
31
+
32
+ def load_jsonc(text: str) -> Any:
33
+ """Parse a JSONC string and return the decoded value."""
34
+ return json.loads(_strip(text))
35
+
36
+
37
+ def load_jsonc_file(path: Path) -> Any:
38
+ """Read + parse a JSONC file."""
39
+ return load_jsonc(path.read_text(encoding="utf-8"))
40
+
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # Stripper
44
+ # ---------------------------------------------------------------------------
45
+
46
+
47
+ def _strip(text: str) -> str:
48
+ """Return ``text`` with comments and trailing commas removed.
49
+
50
+ State machine over characters: outside strings, ``//`` starts a line
51
+ comment and ``/*`` starts a block comment. Inside strings (including
52
+ escapes), everything is preserved verbatim.
53
+ """
54
+ out: list[str] = []
55
+ i = 0
56
+ n = len(text)
57
+ in_string = False
58
+ while i < n:
59
+ ch = text[i]
60
+
61
+ if in_string:
62
+ out.append(ch)
63
+ if ch == "\\" and i + 1 < n:
64
+ out.append(text[i + 1])
65
+ i += 2
66
+ continue
67
+ if ch == '"':
68
+ in_string = False
69
+ i += 1
70
+ continue
71
+
72
+ if ch == '"':
73
+ in_string = True
74
+ out.append(ch)
75
+ i += 1
76
+ continue
77
+
78
+ if ch == "/" and i + 1 < n:
79
+ nxt = text[i + 1]
80
+ if nxt == "/":
81
+ # Line comment — skip to end of line (preserve the newline so
82
+ # error positions stay aligned with the source).
83
+ j = text.find("\n", i + 2)
84
+ i = n if j == -1 else j
85
+ continue
86
+ if nxt == "*":
87
+ # Block comment — skip to matching ``*/``.
88
+ end = text.find("*/", i + 2)
89
+ if end == -1:
90
+ # Unterminated; let json.loads surface the resulting
91
+ # error at this column.
92
+ i = n
93
+ else:
94
+ i = end + 2
95
+ continue
96
+
97
+ out.append(ch)
98
+ i += 1
99
+
100
+ return _strip_trailing_commas("".join(out))
101
+
102
+
103
+ def _strip_trailing_commas(text: str) -> str:
104
+ """Remove commas that immediately precede ``}`` or ``]`` (ignoring whitespace).
105
+
106
+ Same string-aware state machine, in reverse-look style: emit characters
107
+ one at a time, but defer emitting a comma until we see what's next.
108
+ """
109
+ out: list[str] = []
110
+ i = 0
111
+ n = len(text)
112
+ in_string = False
113
+ while i < n:
114
+ ch = text[i]
115
+ if in_string:
116
+ out.append(ch)
117
+ if ch == "\\" and i + 1 < n:
118
+ out.append(text[i + 1])
119
+ i += 2
120
+ continue
121
+ if ch == '"':
122
+ in_string = False
123
+ i += 1
124
+ continue
125
+ if ch == '"':
126
+ in_string = True
127
+ out.append(ch)
128
+ i += 1
129
+ continue
130
+ if ch == ",":
131
+ # Look ahead past whitespace; if next non-space is `}` or `]`,
132
+ # drop the comma.
133
+ j = i + 1
134
+ while j < n and text[j].isspace():
135
+ j += 1
136
+ if j < n and text[j] in "}]":
137
+ i += 1
138
+ continue
139
+ out.append(ch)
140
+ i += 1
141
+ return "".join(out)
@@ -0,0 +1,125 @@
1
+ """Top-level ``stm32`` CLI entry point.
2
+
3
+ Aggregates five per-tool subparser groups — ``prog`` (cubeprogrammer +
4
+ signing per ADR-002 §M1), ``build`` (cubeide), ``mx`` (cubemx), ``debug``
5
+ (gdbserver + arm-gdb), ``vcp`` (USB virtual COM) — and routes parsed args
6
+ to each group's ``dispatch``.
7
+
8
+ Per ``v1/api-conventions.md`` § "Logging and progress streaming", the
9
+ library does NOT configure logging handlers — the CLI does. ``main()``
10
+ installs a stderr handler with a structured-field formatter, scoped to
11
+ the ``embedagents.stm32`` root logger.
12
+
13
+ TODO(v1+): ``--project`` / ``--tools-config`` / ``--defaults-config``
14
+ overrides plumbed into ``SubstrateContext.from_environment``; current Pass-1
15
+ surface uses repo-walked discovery only.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import logging
22
+ import sys
23
+
24
+ from embedagents.stm32 import __version__
25
+ from embedagents.stm32.cli import _build, _debug, _mx, _prog, _vcp
26
+
27
+
28
+ def main(argv: list[str] | None = None) -> int:
29
+ parser = argparse.ArgumentParser(
30
+ prog="stm32",
31
+ description=(
32
+ "STM32 substrate CLI — wraps STM32_Programmer_CLI / CubeIDE / "
33
+ "CubeMX / ST-LINK_gdbserver / arm-none-eabi-gdb / "
34
+ "STM32_SigningTool_CLI behind a unified surface."
35
+ ),
36
+ )
37
+ parser.add_argument(
38
+ "--version",
39
+ action="version",
40
+ version=f"embedagents-stm32 {__version__}",
41
+ )
42
+ parser.add_argument(
43
+ "--pretty",
44
+ action="store_true",
45
+ help="pretty-print JSON output (default: compact)",
46
+ )
47
+ parser.add_argument(
48
+ "-v",
49
+ "--verbose",
50
+ action="count",
51
+ default=0,
52
+ help="increase log verbosity (-v INFO, -vv DEBUG)",
53
+ )
54
+
55
+ subparsers = parser.add_subparsers(
56
+ dest="command",
57
+ metavar="<group>",
58
+ )
59
+ _prog.add_subparser(subparsers)
60
+ _build.add_subparser(subparsers)
61
+ _mx.add_subparser(subparsers)
62
+ _debug.add_subparser(subparsers)
63
+ _vcp.add_subparser(subparsers)
64
+
65
+ if argv is None:
66
+ argv = sys.argv[1:]
67
+ # `stm32 build PATH` ergonomics: route a leading non-action positional
68
+ # into --project before argparse sees the build subtree. The global
69
+ # flags (--pretty, -v) never consume a value, so the first non-flag
70
+ # token is always the command.
71
+ for i, token in enumerate(argv):
72
+ if not token.startswith("-"):
73
+ if token == "build":
74
+ argv = [*argv[: i + 1], *_build.pre_parse_argv(argv[i + 1 :])]
75
+ break
76
+
77
+ args = parser.parse_args(argv)
78
+ _configure_logging(args.verbose)
79
+
80
+ if args.command is None:
81
+ parser.print_help()
82
+ return 0
83
+ if args.command == "prog":
84
+ return _prog.dispatch(args)
85
+ if args.command == "build":
86
+ return _build.dispatch(args)
87
+ if args.command == "mx":
88
+ return _mx.dispatch(args)
89
+ if args.command == "debug":
90
+ return _debug.dispatch(args)
91
+ if args.command == "vcp":
92
+ return _vcp.dispatch(args)
93
+
94
+ # argparse should reject unknown commands before reaching this
95
+ # branch — defensive only.
96
+ parser.error(f"unknown command {args.command!r}")
97
+ return 2 # unreachable; argparse.error raises SystemExit
98
+
99
+
100
+ def _configure_logging(verbosity: int) -> None:
101
+ """Attach a stderr handler to the substrate root logger.
102
+
103
+ ``-v`` → INFO; ``-vv`` → DEBUG; default → WARNING.
104
+ """
105
+ level = logging.WARNING
106
+ if verbosity == 1:
107
+ level = logging.INFO
108
+ elif verbosity >= 2:
109
+ level = logging.DEBUG
110
+
111
+ root = logging.getLogger("embedagents.stm32")
112
+ # Don't double-install when ``main()`` is called multiple times in
113
+ # the same Python process (e.g. tests).
114
+ if not any(getattr(h, "_substrate_installed", False) for h in root.handlers):
115
+ handler = logging.StreamHandler(stream=sys.stderr)
116
+ handler.setFormatter(
117
+ logging.Formatter("%(levelname)s %(name)s: %(message)s")
118
+ )
119
+ handler._substrate_installed = True # type: ignore[attr-defined]
120
+ root.addHandler(handler)
121
+ root.setLevel(level)
122
+
123
+
124
+ if __name__ == "__main__":
125
+ sys.exit(main())
@@ -0,0 +1,300 @@
1
+ """``stm32 build`` CLI subcommand group — cubeide-side operations.
2
+
3
+ Maps to ``v1/cubeide-api.md`` § "CLI subcommand surface". The base
4
+ ``stm32 build`` accepts all simple flags (project / config / clean /
5
+ debug-level / opt / preset / all-configs); action sub-subcommands
6
+ (``add-symbol`` / ``add-lib`` / ``add-source`` / ``add-include``) handle
7
+ the list-shaped edits; discovery sub-subcommands (``in-folder`` /
8
+ ``named``) chain ``find_project`` + ``build``.
9
+
10
+ Output:
11
+
12
+ - Successful build (success=True) → exit 0 with ``BuildResult`` JSON on
13
+ stdout. ``console_output`` mirrored to stderr so users see the build
14
+ log in their terminal.
15
+ - **Build-level failure** (compile / link errors → ``success=False``) →
16
+ **exit 0**: build failure is a result the user scripts check via
17
+ ``BuildResult.success``. console_output still mirrored.
18
+ - Substrate-side failure (``CubeIDEError`` / ``WorkspaceLockedError`` /
19
+ ``CProjectEditError``) → exit 1 with the error JSON on stderr.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import argparse
25
+ import sys
26
+ from pathlib import Path
27
+ from typing import Any
28
+
29
+ from embedagents.stm32.cli._serialize import (
30
+ dumps,
31
+ serialise_error,
32
+ serialise_unexpected,
33
+ )
34
+ from embedagents.stm32.context import SubstrateContext
35
+ from embedagents.stm32.cubeide import CubeIDE
36
+ from embedagents.stm32.errors import SubstrateError
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # Public entry points
41
+ # ---------------------------------------------------------------------------
42
+
43
+
44
+ _BUILD_ACTIONS = frozenset({
45
+ "add-symbol", "add-lib", "add-source",
46
+ "add-include", "in-folder", "named",
47
+ })
48
+
49
+
50
+ def pre_parse_argv(argv: list[str]) -> list[str]:
51
+ """Rewrite ``stm32 build PATH ...`` → ``stm32 build --project PATH ...``.
52
+
53
+ If the first positional token after ``build`` is not a known action
54
+ keyword and not a flag, treat it as a project path and route it
55
+ through ``--project``. Leaves everything else untouched.
56
+
57
+ Caveat: a path whose final component literally matches an action
58
+ keyword (a folder called ``named``, say) dispatches as that action —
59
+ action names win, matching the pre-existing parser behavior.
60
+ """
61
+ # argv at this point starts at the token after "build".
62
+ if not argv:
63
+ return argv
64
+ first = argv[0]
65
+ if first.startswith("-"):
66
+ return argv
67
+ if first in _BUILD_ACTIONS:
68
+ return argv
69
+ # Treat as project path.
70
+ return ["--project", first, *argv[1:]]
71
+
72
+
73
+ def add_subparser(subparsers: argparse._SubParsersAction) -> None:
74
+ """Register the ``build`` group on the top-level parser."""
75
+ parser = subparsers.add_parser(
76
+ "build",
77
+ help="STM32CubeIDE headless build (B-* prompts).",
78
+ )
79
+ _add_common_flags(parser, include_edit_flags=True)
80
+ parser.set_defaults(build_fn=_cmd_base_build)
81
+
82
+ sub = parser.add_subparsers(
83
+ dest="build_action",
84
+ required=False,
85
+ metavar="<action>",
86
+ )
87
+
88
+ # ---- add-symbol ----
89
+ p = sub.add_parser(
90
+ "add-symbol",
91
+ help="B-011 — append preprocessor symbols (one or more NAME[=VALUE]).",
92
+ )
93
+ p.add_argument("symbols", nargs="+", help="NAME or NAME=VALUE")
94
+ _add_common_flags(p, include_edit_flags=False)
95
+ p.set_defaults(build_fn=_cmd_add_symbol)
96
+
97
+ # ---- add-lib ----
98
+ p = sub.add_parser(
99
+ "add-lib",
100
+ help="B-012 — append linker libraries (paths).",
101
+ )
102
+ p.add_argument("libs", nargs="+", type=Path)
103
+ _add_common_flags(p, include_edit_flags=False)
104
+ p.set_defaults(build_fn=_cmd_add_lib)
105
+
106
+ # ---- add-source ----
107
+ p = sub.add_parser(
108
+ "add-source",
109
+ help="B-013 — append source files. v1 records only (tracks aux).",
110
+ )
111
+ p.add_argument("sources", nargs="+", type=Path)
112
+ p.add_argument(
113
+ "--target",
114
+ type=Path,
115
+ default=None,
116
+ help="optional target directory to copy each source into (paired tuple)",
117
+ )
118
+ _add_common_flags(p, include_edit_flags=False)
119
+ p.set_defaults(build_fn=_cmd_add_source)
120
+
121
+ # ---- add-include ----
122
+ p = sub.add_parser(
123
+ "add-include",
124
+ help="B-014 — append compiler include paths.",
125
+ )
126
+ p.add_argument("includes", nargs="+")
127
+ _add_common_flags(p, include_edit_flags=False)
128
+ p.set_defaults(build_fn=_cmd_add_include)
129
+
130
+ # ---- in-folder ----
131
+ p = sub.add_parser(
132
+ "in-folder",
133
+ help="B-018 — discover a project under FOLDER (one match) then build.",
134
+ )
135
+ p.add_argument(
136
+ "folder",
137
+ nargs="?",
138
+ default=None,
139
+ type=Path,
140
+ help="defaults to ctx.cwd",
141
+ )
142
+ p.add_argument("--config", default=None)
143
+ p.add_argument("--clean", action="store_true")
144
+ p.set_defaults(build_fn=_cmd_in_folder)
145
+
146
+ # ---- named ----
147
+ p = sub.add_parser(
148
+ "named",
149
+ help="B-019 — discover by name (exact > substring) then build.",
150
+ )
151
+ p.add_argument("name")
152
+ p.add_argument("--folder", type=Path, default=None)
153
+ p.add_argument("--config", default=None)
154
+ p.add_argument("--clean", action="store_true")
155
+ p.set_defaults(build_fn=_cmd_named)
156
+
157
+
158
+ def dispatch(args: argparse.Namespace) -> int:
159
+ """Run the parsed build subcommand. Returns the process exit code.
160
+
161
+ Build-level failures (``success=False``) exit 0 — the user inspects
162
+ the JSON. Substrate-side failures exit 1 with error JSON on stderr.
163
+ """
164
+ handler = args.build_fn
165
+ try:
166
+ ctx = SubstrateContext.from_environment()
167
+ client = CubeIDE(ctx)
168
+ except SubstrateError as err:
169
+ sys.stderr.write(serialise_error(err) + "\n")
170
+ return 1
171
+ except Exception as err: # CLI boundary: never leak a raw traceback (HARD RULE 1)
172
+ sys.stderr.write(serialise_unexpected(err) + "\n")
173
+ return 2
174
+
175
+ try:
176
+ result = handler(args, client)
177
+ except SubstrateError as err:
178
+ sys.stderr.write(serialise_error(err) + "\n")
179
+ return 1
180
+ except Exception as err: # CLI boundary: never leak a raw traceback (HARD RULE 1)
181
+ sys.stderr.write(serialise_unexpected(err) + "\n")
182
+ return 2
183
+
184
+ # BuildResult.console_output also goes to stderr so the user sees the
185
+ # build log without parsing the JSON envelope.
186
+ if getattr(result, "console_output", None):
187
+ sys.stderr.write(result.console_output)
188
+ if not result.console_output.endswith("\n"):
189
+ sys.stderr.write("\n")
190
+
191
+ sys.stdout.write(dumps(result, pretty=getattr(args, "pretty", False)) + "\n")
192
+ return 0
193
+
194
+
195
+ # ---------------------------------------------------------------------------
196
+ # Shared flag helper
197
+ # ---------------------------------------------------------------------------
198
+
199
+
200
+ def _add_common_flags(parser: argparse.ArgumentParser, *, include_edit_flags: bool) -> None:
201
+ parser.add_argument("--project", type=Path, default=None)
202
+ parser.add_argument("--config", default=None, help="CDT configuration name (e.g. Debug)")
203
+ parser.add_argument("--clean", action="store_true")
204
+ parser.add_argument(
205
+ "--all-configs",
206
+ action="store_true",
207
+ help="apply settings edits to every configuration (not just active)",
208
+ )
209
+ if include_edit_flags:
210
+ parser.add_argument("--debug-level", default=None, dest="debug_level")
211
+ parser.add_argument("--opt", default=None, dest="optimization")
212
+ parser.add_argument("--preset", default=None, help="fast / size / balanced")
213
+
214
+
215
+ def _common_kwargs(args: argparse.Namespace) -> dict[str, Any]:
216
+ kwargs: dict[str, Any] = {
217
+ "project": getattr(args, "project", None),
218
+ "configuration": getattr(args, "config", None),
219
+ "clean": getattr(args, "clean", False),
220
+ }
221
+ if getattr(args, "all_configs", False):
222
+ kwargs["modify_all_configurations"] = True
223
+ return kwargs
224
+
225
+
226
+ # ---------------------------------------------------------------------------
227
+ # Handlers
228
+ # ---------------------------------------------------------------------------
229
+
230
+
231
+ def _cmd_base_build(args: argparse.Namespace, client: CubeIDE) -> Any:
232
+ """``stm32 build`` (no sub-subcommand) — base build with simple flags."""
233
+ kwargs = _common_kwargs(args)
234
+ kwargs["debug_level"] = getattr(args, "debug_level", None)
235
+ kwargs["optimization"] = getattr(args, "optimization", None)
236
+ kwargs["preset"] = getattr(args, "preset", None)
237
+ return client.build(**kwargs)
238
+
239
+
240
+ def _cmd_add_symbol(args: argparse.Namespace, client: CubeIDE) -> Any:
241
+ parsed = [_parse_symbol(s) for s in args.symbols]
242
+ kwargs = _common_kwargs(args)
243
+ kwargs["add_symbols"] = parsed
244
+ return client.build(**kwargs)
245
+
246
+
247
+ def _cmd_add_lib(args: argparse.Namespace, client: CubeIDE) -> Any:
248
+ kwargs = _common_kwargs(args)
249
+ kwargs["add_libraries"] = list(args.libs)
250
+ return client.build(**kwargs)
251
+
252
+
253
+ def _cmd_add_source(args: argparse.Namespace, client: CubeIDE) -> Any:
254
+ sources: list = list(args.sources)
255
+ if args.target is not None:
256
+ sources = [(p, args.target) for p in sources]
257
+ kwargs = _common_kwargs(args)
258
+ kwargs["add_sources"] = sources
259
+ return client.build(**kwargs)
260
+
261
+
262
+ def _cmd_add_include(args: argparse.Namespace, client: CubeIDE) -> Any:
263
+ kwargs = _common_kwargs(args)
264
+ kwargs["add_include_paths"] = list(args.includes)
265
+ return client.build(**kwargs)
266
+
267
+
268
+ def _cmd_in_folder(args: argparse.Namespace, client: CubeIDE) -> Any:
269
+ found = client.find_project(folder=args.folder)
270
+ return client.build(
271
+ project=found.path,
272
+ configuration=args.config,
273
+ clean=args.clean,
274
+ )
275
+
276
+
277
+ def _cmd_named(args: argparse.Namespace, client: CubeIDE) -> Any:
278
+ found = client.find_project(folder=args.folder, name=args.name)
279
+ return client.build(
280
+ project=found.path,
281
+ configuration=args.config,
282
+ clean=args.clean,
283
+ )
284
+
285
+
286
+ # ---------------------------------------------------------------------------
287
+ # Parsing helpers
288
+ # ---------------------------------------------------------------------------
289
+
290
+
291
+ def _parse_symbol(s: str) -> str | tuple[str, str]:
292
+ """Parse ``NAME`` or ``NAME=VALUE`` for ``add-symbol``."""
293
+ if "=" not in s:
294
+ return s
295
+ name, _, value = s.partition("=")
296
+ if not name:
297
+ raise argparse.ArgumentTypeError(
298
+ f"add-symbol expects NAME or NAME=VALUE, got {s!r}"
299
+ )
300
+ return (name, value)