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.
- embedagents/stm32/__init__.py +15 -0
- embedagents/stm32/_jsonc.py +141 -0
- embedagents/stm32/cli/__init__.py +125 -0
- embedagents/stm32/cli/_build.py +300 -0
- embedagents/stm32/cli/_debug.py +525 -0
- embedagents/stm32/cli/_mx.py +102 -0
- embedagents/stm32/cli/_prog.py +664 -0
- embedagents/stm32/cli/_serialize.py +108 -0
- embedagents/stm32/cli/_vcp.py +193 -0
- embedagents/stm32/context.py +647 -0
- embedagents/stm32/cubeide/__init__.py +44 -0
- embedagents/stm32/cubeide/client.py +1009 -0
- embedagents/stm32/cubeide/cproject.py +634 -0
- embedagents/stm32/cubeide/headless.py +161 -0
- embedagents/stm32/cubeide/presets.py +124 -0
- embedagents/stm32/cubeide/results.py +106 -0
- embedagents/stm32/cubeide/workspace.py +220 -0
- embedagents/stm32/cubemx/__init__.py +22 -0
- embedagents/stm32/cubemx/client.py +253 -0
- embedagents/stm32/cubemx/launcher.py +60 -0
- embedagents/stm32/cubemx/results.py +51 -0
- embedagents/stm32/cubemx/runner.py +350 -0
- embedagents/stm32/cubeprogrammer/__init__.py +65 -0
- embedagents/stm32/cubeprogrammer/client.py +1629 -0
- embedagents/stm32/cubeprogrammer/codes.py +68 -0
- embedagents/stm32/cubeprogrammer/diagnose.py +156 -0
- embedagents/stm32/cubeprogrammer/external_loader.py +84 -0
- embedagents/stm32/cubeprogrammer/parsers.py +897 -0
- embedagents/stm32/cubeprogrammer/results.py +293 -0
- embedagents/stm32/debug/__init__.py +72 -0
- embedagents/stm32/debug/client.py +320 -0
- embedagents/stm32/debug/gdb.py +432 -0
- embedagents/stm32/debug/gdbserver.py +355 -0
- embedagents/stm32/debug/parsers.py +571 -0
- embedagents/stm32/debug/pipereader.py +76 -0
- embedagents/stm32/debug/results.py +254 -0
- embedagents/stm32/debug/session.py +641 -0
- embedagents/stm32/debug/svd.py +670 -0
- embedagents/stm32/errors.py +304 -0
- embedagents/stm32/logging_setup.py +38 -0
- embedagents/stm32/platform/__init__.py +43 -0
- embedagents/stm32/platform/locking.py +172 -0
- embedagents/stm32/platform/process.py +254 -0
- embedagents/stm32/progress.py +34 -0
- embedagents/stm32/py.typed +0 -0
- embedagents/stm32/resolution.py +79 -0
- embedagents/stm32/schemas/__init__.py +0 -0
- embedagents/stm32/schemas/stm32-project.schema.json +189 -0
- embedagents/stm32/schemas/stm32-runtime-defaults.schema.json +264 -0
- embedagents/stm32/schemas/stm32-tools.local.schema.json +114 -0
- embedagents/stm32/signing/__init__.py +23 -0
- embedagents/stm32/signing/client.py +302 -0
- embedagents/stm32/signing/results.py +33 -0
- embedagents/stm32/subprocess_runner.py +229 -0
- embedagents/stm32/vcp/__init__.py +40 -0
- embedagents/stm32/vcp/client.py +451 -0
- embedagents/stm32/vcp/discovery.py +84 -0
- embedagents/stm32/vcp/reader.py +483 -0
- embedagents/stm32/vcp/results.py +84 -0
- embedagents_stm32-0.3.0.dist-info/METADATA +315 -0
- embedagents_stm32-0.3.0.dist-info/RECORD +65 -0
- embedagents_stm32-0.3.0.dist-info/WHEEL +5 -0
- embedagents_stm32-0.3.0.dist-info/entry_points.txt +2 -0
- embedagents_stm32-0.3.0.dist-info/licenses/LICENSE +21 -0
- 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)
|