Coverage for /builds/svesch/cikit/src/cikit/macros/platformio.py: 99%
109 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-10-09 14:17 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-10-09 14:17 +0000
1# SPDX-License-Identifier: GPL-3.0-or-later
2"""PlatformIO macro helpers for cikit.
4This module builds PlatformIO projects, extracts build metrics from command
5output, stores command logs, and generates build reports. Project configuration
6remains entirely in platformio.ini.
8Author:
9 Sven Schlender
10Date:
11 2026-09-13
12"""
14import json
15import re
16import time
17import shutil
18from pathlib import Path
19from tempfile import TemporaryDirectory
21from cikit.artifacts import Artifact, ArtifactType
22from cikit.command import Command, run_command
23from cikit.errors import CiToolsError
24from .reports import write_build_report
27PLATFORMIO_BUILD_LOG = "platformio-build.log"
29PLATFORMIO_COMPILE_COMMANDS = "compile_commands.json"
31ERROR_PATTERN = re.compile(r"\berror:\s", re.IGNORECASE)
33MEMORY_PATTERN = re.compile(
34 r"^(?P<region>[A-Za-z0-9_ -]+):\s+"
35 r"\[[^\]]*\]\s+"
36 r"(?P<percent>\d+(?:\.\d+)?)%\s+"
37 r"\(used\s+(?P<used>\d+)\s+bytes\s+from\s+(?P<total>\d+)\s+bytes\)$"
38)
40WARNING_PATTERN = re.compile(r"\bwarning:\s", re.IGNORECASE)
43def _normalize_memory_region(region: str) -> str:
44 """Normalize a PlatformIO memory region name for cikit data keys.
46 Region names are converted to lowercase and non-alphanumeric separators
47 are replaced with underscores. This keeps generated run data keys stable
48 and suitable for timeline queries.
50 Args:
51 region: Memory region name reported by PlatformIO.
53 Returns:
54 Normalized memory region name.
55 """
56 region = region.strip().lower()
57 region = re.sub(r"[^a-z0-9]+", "_", region)
59 return region.strip("_")
62def _parse_build_output(stdout: str, stderr: str) -> dict:
63 """Extract build metrics from PlatformIO output.
65 Standard output is parsed for compiled files and memory usage. Standard
66 error is parsed separately for compiler warnings and errors. File,
67 warning, and error counts are always returned, while memory metrics are
68 only added when detected.
70 Args:
71 stdout: Captured PlatformIO standard output.
72 stderr: Captured PlatformIO standard error.
74 Returns:
75 Dictionary containing the extracted build metrics.
76 """
77 data = {}
78 build_files = 0
80 for line in stdout.splitlines():
81 stripped = line.strip()
83 if stripped.startswith("Compiling "):
84 build_files += 1
86 match = MEMORY_PATTERN.match(stripped)
87 if not match:
88 continue
90 region = _normalize_memory_region(match.group("region"))
91 prefix = f"memory_{region}"
93 data[f"{prefix}_used"] = int(match.group("used"))
94 data[f"{prefix}_total"] = int(match.group("total"))
95 data[f"{prefix}_percent"] = float(match.group("percent"))
97 data["build_files"] = build_files
99 data["warnings"] = sum(1 for line in stderr.splitlines() if WARNING_PATTERN.search(line))
100 data["errors"] = sum(1 for line in stderr.splitlines() if ERROR_PATTERN.search(line))
102 return data
105def _get_project_environment(env, project_dir: Path) -> dict:
106 """Return metadata for the resolved PlatformIO environment.
108 Args:
109 env: Resolved CI run environment.
110 project_dir: PlatformIO project directory.
112 Returns:
113 Metadata of the resolved PlatformIO environment.
115 Raises:
116 CiToolsError: If PlatformIO metadata cannot be queried or parsed, or
117 the build environment cannot be determined unambiguously.
118 """
119 cmd = Command("pio", use_exec_tool=True)
120 cmd.add_subcommand("metadata", ["project", "metadata", "--json-output"])
122 result = run_command(env.argv(cmd, "metadata"), cwd=project_dir, live_output=False,
123 check_exit_code=False)
125 result.check_returncode()
127 try:
128 metadata = json.loads(result.stdout)
129 except json.JSONDecodeError as exp:
130 raise CiToolsError("Invalid PlatformIO project metadata") from exp
132 if not isinstance(metadata, dict) or len(metadata) != 1:
133 raise CiToolsError("PlatformIO build environment cannot be determined unambiguously")
135 return next(iter(metadata.values()))
138def gather_compiler_meta(env, project_dir: Path = None):
139 """Gather compiler metadata for a PlatformIO project.
141 PlatformIO project metadata is used to determine the compiler selected for
142 the resolved build environment. The compiler executable is queried for its
143 actual version.
145 Detected metadata is stored directly in the run data. No data is added if
146 the compiler cannot be determined unambiguously.
148 Args:
149 env: Resolved CI run environment.
150 project_dir: Optional PlatformIO project directory.
151 """
152 project_dir = project_dir or env["PROJECT_ROOT"]
154 try:
155 environment = _get_project_environment(env, project_dir)
156 except CiToolsError:
157 return
159 compiler_path = environment.get("cc_path")
160 if not compiler_path:
161 return
163 compiler_path = Path(compiler_path)
165 compiler_cmd = Command(str(compiler_path), ["-dumpfullversion", "-dumpversion"])
167 version_result = run_command(env.argv(compiler_cmd), cwd=project_dir, live_output=False,
168 check_exit_code=False)
170 if version_result.returncode != 0:
171 return
173 compiler_version = version_result.stdout.strip()
174 if not compiler_version:
175 return
177 compiler_meta = {"compiler": compiler_path.name.removesuffix(".exe"),
178 "compiler_version": compiler_version}
180 compiler_type = environment.get("compiler_type")
181 if compiler_type: 181 ↛ 184line 181 didn't jump to line 184 because the condition on line 181 was always true
182 compiler_meta["compiler_type"] = compiler_type
184 env.data_update(compiler_meta)
187def get_build_dir(env, project_dir: Path = None) -> Path:
188 """Return the build directory of the resolved PlatformIO environment.
190 Args:
191 env: Resolved CI run environment.
192 project_dir: Optional PlatformIO project directory.
194 Returns:
195 Absolute path to the PlatformIO environment build directory.
197 Raises:
198 CiToolsError: If PlatformIO metadata cannot be queried or the program
199 path cannot be determined.
200 """
201 project_dir = project_dir or env["PROJECT_ROOT"]
203 environment = _get_project_environment(env, project_dir)
205 program_path = environment.get("prog_path")
206 if not program_path:
207 raise CiToolsError("PlatformIO metadata does not contain a program path")
209 return Path(program_path).parent
212def generate_compile_commands(env, project_dir: Path = None) -> Artifact:
213 """Generate and register the PlatformIO compilation database.
215 PlatformIO generates ``compile_commands.json`` from the configured build
216 environment. The resulting compilation database is registered as a
217 metadata artifact for use by static analysis tools.
219 Args:
220 env: Resolved CI run environment.
221 project_dir: Optional PlatformIO project directory.
223 Returns:
224 Created and registered compilation database artifact.
226 Raises:
227 CiToolsError: If PlatformIO cannot be started, generation fails, or the
228 compilation database was not created.
229 """
230 project_dir = project_dir or env["PROJECT_ROOT"]
232 cmd = Command("pio", use_exec_tool=True)
233 cmd.add_subcommand("compiledb", ["run", "-t", "compiledb"])
235 result = run_command(env.argv(cmd, "compiledb"), cwd=project_dir, live_output=True,
236 check_exit_code=False)
238 result.check_returncode()
240 compile_commands = project_dir / PLATFORMIO_COMPILE_COMMANDS
242 if not compile_commands.is_file():
243 raise CiToolsError(f"PlatformIO did not generate {PLATFORMIO_COMPILE_COMMANDS}")
245 return env.copy_artifact(compile_commands, ArtifactType.METADATA)
248def build(env, project_dir: Path = None, clean: bool = False):
249 """Build a PlatformIO project and store extracted build metrics.
251 PlatformIO resolves build environments, boards, frameworks, libraries,
252 and all other project settings from platformio.ini. The optional clean
253 mode removes the local PlatformIO build directory before building.
255 Args:
256 env: Resolved CI run environment.
257 project_dir: Optional PlatformIO project directory.
258 clean: Remove the PlatformIO build directory before building.
260 Raises:
261 CiToolsError: If PlatformIO cannot be started or the build fails.
262 """
263 project_dir = project_dir or env["PROJECT_ROOT"]
265 if clean:
266 build_dir = project_dir / ".pio"
267 shutil.rmtree(build_dir, ignore_errors=True)
269 cmd = Command("pio", use_exec_tool=True)
270 cmd.add_subcommand("run", ["run"])
272 start_time = time.monotonic()
273 result = run_command(env.argv(cmd, "run"), cwd=project_dir, live_output=True,
274 check_exit_code=False)
275 build_time = round(time.monotonic() - start_time, 1)
277 metrics = _parse_build_output(result.stdout, result.stderr)
278 metrics["build_time"] = build_time
280 env.data_update(metrics)
282 with TemporaryDirectory() as temp_dir:
283 log_file = Path(temp_dir) / PLATFORMIO_BUILD_LOG
284 result.write_report(log_file)
285 env.copy_artifact(log_file, ArtifactType.LOG)
287 result.check_returncode()
290def write_rst(env, name: str = None, report_title: str = None) -> Artifact:
291 """Create an RST report for a PlatformIO build.
293 Args:
294 env: Resolved CI run environment.
295 name: Output file basename without extension. If omitted, the run name
296 is used.
297 report_title: Report title. If omitted, ``PlatformIO Build Report`` is
298 used.
300 Returns:
301 Created and registered RST artifact.
302 """
303 return write_build_report(env, name=name,
304 report_title=report_title or "PlatformIO Build Report")