Compare commits

...
Author SHA1 Message Date
J. Nick Koston 5184cc1ace Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-09-03 23:48:52 +02:00
J. Nick Koston a59ab05bc8 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain
# Conflicts:
#	esphome/components/esp8266/__init__.py
2026-09-03 23:48:38 +02:00
J. Nick Koston 71f9c159a8 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-09-03 23:46:48 +02:00
J. Nick Koston bbd66c61da Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-09-02 11:18:07 +02:00
J. Nick Koston a5a3163b5d Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain
# Conflicts:
#	tests/script/test_determine_jobs.py
2026-09-02 11:17:59 +02:00
J. Nick Koston e547af259e Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-09-02 11:17:14 +02:00
J. Nick Koston 60a2bdb935 Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-30 14:57:39 -05:00
J. Nick Koston 2a860769ce Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-30 14:57:39 -05:00
J. Nick Koston da95a657e1 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-30 14:57:39 -05:00
J. Nick Koston de3e4430d3 Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-28 20:52:38 -05:00
J. Nick Koston b1d359c4a9 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-28 20:52:38 -05:00
J. Nick Koston f3d6bc89a4 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-28 20:52:38 -05:00
J. Nick Koston d0a66d26fd Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-28 20:45:03 -05:00
J. Nick Koston 22f4adbb45 Keep espidf-only infra out of the esp8266 native trigger set
The dev merge moved the ESP-IDF trigger constants into clang_tidy_hash;
the esp8266 set keeps its explicit shared-module list with a subset
tripwire against the imported set instead of unioning it wholesale.
2026-08-28 20:44:53 -05:00
J. Nick Koston f2ee227070 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain
# Conflicts:
#	script/determine-jobs.py
2026-08-28 20:44:18 -05:00
J. Nick Koston ad92a56f07 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-28 20:43:17 -05:00
J. Nick Koston a817a01097 Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-28 20:41:31 -05:00
J. Nick Koston de60f9730e Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-28 20:41:31 -05:00
J. Nick Koston e4ca495c4f Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-28 20:41:31 -05:00
J. Nick Koston 5f273cef7f Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-28 13:52:00 -05:00
J. Nick Koston 9f3842a0e0 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-28 13:52:00 -05:00
J. Nick Koston 36e38125c6 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-28 13:51:59 -05:00
J. Nick Koston 4137511f81 Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-28 13:07:05 -05:00
J. Nick Koston eb1a3bc9e4 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-28 13:07:05 -05:00
J. Nick Koston 0a595aab03 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-28 13:07:05 -05:00
J. Nick Koston 880ffc609d Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-27 23:01:18 -05:00
J. Nick Koston 979aac1cae Silence the framework elf2bin script's SyntaxWarning
The bundled elf2bin.py uses unescaped regex strings that Python 3.12+
flags at compile time; the frozen 3.1.2 framework will never fix it,
so the ninja rule suppresses that category for the one invocation.
2026-08-27 23:01:15 -05:00
J. Nick Koston 95ea16b8c4 Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-27 15:52:59 -05:00
J. Nick Koston b29b203ab1 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-27 15:52:58 -05:00
J. Nick Koston 31470e7997 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-27 15:52:58 -05:00
J. Nick Koston c817bdd25e Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-27 14:04:02 -05:00
J. Nick Koston 238238e8a1 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-27 14:04:01 -05:00
J. Nick Koston 7f77764dbb Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-27 14:04:01 -05:00
J. Nick Koston 9d3a68cd33 Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-27 12:38:31 -05:00
J. Nick Koston bb823ba4c0 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-27 12:38:24 -05:00
J. Nick Koston 1132ec92e8 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-27 12:38:24 -05:00
J. Nick Koston 85142b5b99 Pass -MF to the probe only alongside a dependency flag 2026-08-26 22:22:42 -05:00
J. Nick Koston a5afa55122 Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch
# Conflicts:
#	esphome/platformio/toolchain.py
2026-08-26 22:06:01 -05:00
J. Nick Koston 3439a2d5ca Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-26 22:05:33 -05:00
J. Nick Koston c2f8347eed Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-26 22:05:32 -05:00
J. Nick Koston 3dd608cffc Use a drive-qualified basedir in the test on Windows 2026-08-26 14:23:16 -05:00
J. Nick Koston 1d4d28832e Reject relative CCACHE_BASEDIR values too 2026-08-26 14:13:57 -05:00
J. Nick Koston 26e51c23b1 Hoist is_joined_include, cover the falsy EXTSUM warning 2026-08-26 13:21:21 -05:00
J. Nick Koston 6530bb62f8 Stat-aware local walk, atomic flag+env application, guard degenerate basedir and -include- flags 2026-08-26 13:14:03 -05:00
J. Nick Koston 3730f7bbae Distinguish stat failure from absence, fail closed on unmodelable flags and unreadable local headers 2026-08-26 11:58:20 -05:00
J. Nick Koston d0263b0bef Mirror the joined -include fold, harden the probe, fail closed on unreadable closure headers 2026-08-26 10:44:17 -05:00
J. Nick Koston 410a3868ea Gate the ccache sloppiness on the pch actually being emitted 2026-08-26 10:00:59 -05:00
J. Nick Koston a69f2c8f66 Fold the joined src force-include spelling, export ccache settings after the flags land 2026-08-26 09:57:44 -05:00
J. Nick Koston c9b4086a92 Trim comments and docstrings to repo standard 2026-08-26 09:33:06 -05:00
J. Nick Koston 397ae10c55 Demote invalid-pch on PlatformIO too, pin the compile locale, document the .sum invariant 2026-08-26 09:07:38 -05:00
J. Nick Koston 7417dcdd7b Demote invalid-pch under user -Werror, catch the joined -include spelling, treat resource exhaustion as transient 2026-08-26 08:39:27 -05:00
J. Nick Koston f5593deb38 Warn and skip the pch when a user -include precedes it, walk angle includes, read sidecars defensively 2026-08-26 00:46:14 -05:00
J. Nick Koston f8f12b3a5d Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-26 00:06:43 -05:00
J. Nick Koston 85dd29f7e0 Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-26 00:06:43 -05:00
J. Nick Koston 21896b057f Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-26 00:06:42 -05:00
J. Nick Koston c9044bb860 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-25 23:42:30 -05:00
J. Nick Koston c2767c096c Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-25 23:42:30 -05:00
J. Nick Koston c86b038dc1 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-25 23:42:30 -05:00
J. Nick Koston 55f1122eba Merge remote-tracking branch 'origin/esp8266-native-pch' into esp8266-native-pch 2026-08-25 23:16:52 -05:00
J. Nick Koston fb640bdc47 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-25 23:16:32 -05:00
J. Nick Koston 7aa90fc4c4 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-25 23:16:32 -05:00
J. Nick Koston 20e5b23b86 Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-25 23:16:32 -05:00
J. Nick Koston 10204c6bd9 Surrogateescape the digest extras, keep absolute force-includes consumer-only 2026-08-25 23:02:43 -05:00
J. Nick Koston 8e5d72a133 Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-25 22:55:24 -05:00
J. Nick Koston ccaec42798 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-25 22:55:23 -05:00
J. Nick Koston 5755e47101 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-25 22:55:23 -05:00
J. Nick Koston 5a1f3a382d Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-25 22:53:02 -05:00
J. Nick Koston 813008200c Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-25 22:53:02 -05:00
J. Nick Koston f217ab18d2 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-25 22:53:02 -05:00
J. Nick Koston 83eede0d9d Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-25 22:40:46 -05:00
J. Nick Koston 32c3bf7f15 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-25 22:40:46 -05:00
J. Nick Koston f21310d517 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-25 22:40:45 -05:00
J. Nick Koston df8fb0fb49 Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-25 22:39:45 -05:00
J. Nick Koston 1895bef123 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-25 22:39:44 -05:00
J. Nick Koston e18e8deded Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-25 22:39:44 -05:00
J. Nick Koston bf849f441f Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-25 22:29:40 -05:00
J. Nick Koston 97bf416a2b Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-25 22:29:39 -05:00
J. Nick Koston d3d81f593b Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-25 22:29:39 -05:00
J. Nick Koston fc654c4c63 Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-25 22:23:07 -05:00
J. Nick Koston 204cec24a1 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-25 22:23:07 -05:00
J. Nick Koston d87e666f02 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-25 22:23:07 -05:00
J. Nick Koston 3e9b6b0355 Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-25 22:04:06 -05:00
J. Nick Koston 5b2670ea4f Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-25 22:04:05 -05:00
J. Nick Koston 706e3a1a39 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-25 22:04:05 -05:00
J. Nick Koston 5a8e6761cb Fold only src-resolvable force-includes, surrogateescape the digest names, harden flag lexing and failure reporting 2026-08-25 21:44:52 -05:00
J. Nick Koston 06556ceac5 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-25 21:33:56 -05:00
J. Nick Koston 9db1835c40 Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-25 21:33:56 -05:00
J. Nick Koston fc23fe5229 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-25 21:33:55 -05:00
J. Nick Koston 73cc3af130 Union sloppiness tokens on the PlatformIO path too, survive non-UTF-8 include names 2026-08-25 20:56:00 -05:00
J. Nick Koston 6aa5540ff8 Cite esphome#8693 for the intended Arduino.h visibility 2026-08-25 20:40:20 -05:00
J. Nick Koston 2e7aa4d534 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-25 20:26:28 -05:00
J. Nick Koston 26d7a75352 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-25 20:26:28 -05:00
J. Nick Koston b5dd3d00b8 Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-25 20:26:28 -05:00
J. Nick Koston c419fc4bbc Skip the pch when a header identity is unknown, union missing sloppiness tokens, report projenv import failures 2026-08-25 19:59:25 -05:00
J. Nick Koston 9636717b32 Warn on unresolvable force-includes and pch_defines-less sloppiness, vary the unreadable marker 2026-08-25 19:08:21 -05:00
J. Nick Koston e5c6720063 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-25 18:20:36 -05:00
J. Nick Koston 4dc10199b9 Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-25 18:20:36 -05:00
J. Nick Koston 5b92b381c3 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-25 18:20:35 -05:00
J. Nick Koston aee60fa48a Merge remote-tracking branch 'origin/esp8266-native-pch' into esp8266-native-pch 2026-08-25 18:19:26 -05:00
J. Nick Koston 7cc53d90bd Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-25 18:18:23 -05:00
J. Nick Koston 7b7acf7656 Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-25 18:18:23 -05:00
J. Nick Koston e8a8226bac Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-25 18:18:22 -05:00
J. Nick Koston 1b54d1b5b0 Survive -t nobuild, skip library trees in the local include digest, drop the false ENV scoping claim 2026-08-25 18:13:15 -05:00
J. Nick Koston b6c1552bcb Simplify: shared artifact names, one cxx override param, extracted gch compile, transient spawn semantics in the script 2026-08-25 17:56:23 -05:00
J. Nick Koston 564f3e31c1 Harden idedata -include anchoring, probe exit check, package identity, and failure diagnostics 2026-08-25 17:44:54 -05:00
J. Nick Koston a4a8c77151 Resolve idedata force-include paths, test probe rejection and env scoping, derive clean names 2026-08-25 16:19:29 -05:00
J. Nick Koston c1dfecf964 Cover the partial-clean pch artifact removal 2026-08-25 15:43:18 -05:00
J. Nick Koston d08c524f19 Harden the pch script for Windows paths and make ignored pchs visible 2026-08-25 15:36:41 -05:00
pre-commit-ci-lite[bot] 18eb05177b apply automatic formatting fixes 2026-08-25 20:30:53 +00:00
J. Nick Koston c50a1a068c Merge branch 'esp8266-native-pch' of https://github.com/esphome/esphome into esp8266-native-pch 2026-08-25 15:28:04 -05:00
J. Nick Koston dc45cd4d3f Probe that the toolchain can load the gch before enabling the pch 2026-08-25 15:27:53 -05:00
J. Nick Koston 6c98f2a177 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-25 15:17:13 -05:00
J. Nick Koston bf573413c4 Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-25 15:17:13 -05:00
J. Nick Koston c6217235e3 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-25 15:17:13 -05:00
J. Nick Koston fae9eb0dd2 Hash project-local include dirs into the pch checksum and share the registration gate 2026-08-25 15:04:55 -05:00
J. Nick Koston 6ab93051b7 Address review: order-covering checksum, loud degraded states, script tests 2026-08-25 14:28:57 -05:00
J. Nick Koston 7d91938b79 Reconstruct compiler argv per SCons element for the gch compile 2026-08-25 14:16:04 -05:00
J. Nick Koston ed59080f62 Tokenize SCons flags per element so spaced defines survive 2026-08-25 14:09:22 -05:00
J. Nick Koston a63c5b3680 Merge branch 'esp8266-native-pch' of https://github.com/esphome/esphome into esp8266-native-pch 2026-08-25 14:04:38 -05:00
J. Nick Koston 558acf6900 Make the PlatformIO pch script platform-agnostic 2026-08-25 14:04:29 -05:00
J. Nick Koston 32e11a95cb Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-25 14:04:02 -05:00
J. Nick Koston 911b4afa08 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-25 14:04:02 -05:00
J. Nick Koston 1cb86be73d Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-25 14:04:02 -05:00
J. Nick Koston f18b182606 Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-25 13:46:34 -05:00
J. Nick Koston 43c389e1fa Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-25 13:46:34 -05:00
J. Nick Koston f4808e291e Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-25 13:46:34 -05:00
J. Nick Koston 7f6a007d27 Add direct tests for the shared pch helpers 2026-08-25 13:44:24 -05:00
J. Nick Koston 0e54f66e78 Merge remote-tracking branch 'origin/esp8266-native-pch' into esp8266-native-pch 2026-08-25 13:39:49 -05:00
J. Nick Koston de7f622983 Merge branch 'esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-25 13:37:27 -05:00
J. Nick Koston 986c0563b2 Merge remote-tracking branch 'origin/esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-25 13:36:15 -05:00
J. Nick Koston 6c15897d26 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-25 13:34:52 -05:00
J. Nick Koston 6a39f1509f Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-25 13:34:52 -05:00
J. Nick Koston 2f1d21191e Merge remote-tracking branch 'origin/esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-25 13:32:52 -05:00
J. Nick Koston 83a2f78075 Walk the include closure in the pch checksum and honor a user CCACHE_BASEDIR 2026-08-25 13:32:17 -05:00
J. Nick Koston e090311ac2 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-25 13:21:58 -05:00
J. Nick Koston 5e743ee301 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-25 13:21:58 -05:00
J. Nick Koston aaf76710a9 Merge remote-tracking branch 'origin/esp8266-arduino-toolchain' into esp8266-native-pch 2026-08-25 13:16:51 -05:00
J. Nick Koston 2665a4fa12 [esp8266] Precompile the shared prefix header for src compiles 2026-08-25 13:16:51 -05:00
J. Nick Koston d77be85afa Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-25 12:49:56 -05:00
J. Nick Koston 41f97a16db Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-25 12:49:56 -05:00
J. Nick Koston dccd4e91d2 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-25 12:39:03 -05:00
J. Nick Koston d018f5a720 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-25 12:39:03 -05:00
J. Nick Koston c91fa2b6fc Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-25 12:37:47 -05:00
J. Nick Koston e2c2b2f53e Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-25 12:37:47 -05:00
J. Nick Koston 422d1c67da Honor bulk in every per-address decode marker 2026-08-25 11:54:33 -05:00
J. Nick Koston 6c14bff747 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-25 10:47:59 -05:00
J. Nick Koston e5d95bafd0 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-25 10:47:59 -05:00
J. Nick Koston a0dcb6a5f5 Demote bulk stack-word decode misses to debug, count built components per pattern 2026-08-25 08:37:33 -05:00
J. Nick Koston 2ee448a7a8 Cover the failed-probe fall-through 2026-08-24 19:19:30 -05:00
J. Nick Koston f934f3ac7e Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-24 18:53:58 -05:00
J. Nick Koston fd3654e206 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-24 18:53:58 -05:00
J. Nick Koston 85c0d86040 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain
# Conflicts:
#	esphome/components/esp8266/__init__.py
2026-08-24 18:52:51 -05:00
J. Nick Koston 35d147aef1 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission
# Conflicts:
#	esphome/components/esp8266/const.py
2026-08-24 18:52:13 -05:00
J. Nick Koston 77de13dab3 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-24 18:48:02 -05:00
J. Nick Koston 8e3aa5e921 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-24 18:48:02 -05:00
J. Nick Koston 260bfc2358 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-24 18:47:07 -05:00
J. Nick Koston 4e66248682 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-24 18:47:07 -05:00
J. Nick Koston a5ae1097cc Restore the fast fail before the baseline build when nothing matched 2026-08-24 18:45:40 -05:00
J. Nick Koston 1739bbf3de Name explicit ninja targets instead of policing mtimes, unify the no-tests accounting per pattern, mark stale-ELF decodes, glob the idedata caches 2026-08-24 18:15:58 -05:00
J. Nick Koston f30802757e Stamp the compile-DB freshness, defer the silent-component failure past the summary, mark every undecoded address, honor all-platform fixtures 2026-08-24 17:06:59 -05:00
J. Nick Koston eb2e9a791b Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-24 16:00:09 -05:00
J. Nick Koston 7e2c83a397 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-24 16:00:09 -05:00
J. Nick Koston 8d1c58afaa Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-24 15:58:32 -05:00
J. Nick Koston 37d5ba1807 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-24 15:58:32 -05:00
J. Nick Koston 4b62689952 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-24 14:39:01 -05:00
J. Nick Koston 4fb83364e8 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-24 14:39:01 -05:00
J. Nick Koston 634dfe6f3c Harden the skip path against stale artifacts, tolerate corrupt linker scripts, guard the baseline degrade, mark undecoded addresses 2026-08-24 13:49:50 -05:00
J. Nick Koston 0556c212b2 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-24 13:36:49 -05:00
J. Nick Koston 9b02163b99 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission
# Conflicts:
#	tests/unit_tests/build_gen/test_arduino8266.py
2026-08-24 13:36:39 -05:00
J. Nick Koston 6068d7887f Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-24 12:32:55 -05:00
J. Nick Koston 7dddad5471 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-24 12:32:55 -05:00
J. Nick Koston e50057f7bb Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-24 12:20:54 -05:00
J. Nick Koston 738f5b1798 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-24 12:20:54 -05:00
J. Nick Koston af500d6bec Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 22:25:38 -05:00
J. Nick Koston efb8f5ff1a Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 22:25:38 -05:00
J. Nick Koston ac3a6934f6 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 22:02:14 -05:00
J. Nick Koston 7db3fd1286 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 22:02:14 -05:00
J. Nick Koston 1780a54524 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 22:00:36 -05:00
J. Nick Koston 6b5b8e0fd0 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 22:00:35 -05:00
J. Nick Koston 1caad91260 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 21:10:49 -05:00
J. Nick Koston 9ee07ab960 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 21:10:49 -05:00
J. Nick Koston 985a784f5e Point analyze-memory at the platformio escape hatch, test the backend-table invariant instead of asserting at import, fail blank component lists under the flag, clean the arduino idedata cache 2026-08-23 20:53:55 -05:00
J. Nick Koston 0b920477b3 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 19:43:43 -05:00
J. Nick Koston bae1291d73 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 19:43:43 -05:00
J. Nick Koston 4a9dfe0456 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 19:38:34 -05:00
J. Nick Koston f973c7bfb7 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 19:38:34 -05:00
J. Nick Koston 9c2a3baaa9 Name the platform in the analyze-memory refusal, surface parser-shaped idedata failures, count builds per component under the no-tests flag 2026-08-23 19:38:12 -05:00
J. Nick Koston f3851bf3ad Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 19:30:47 -05:00
J. Nick Koston b9d40d2b1b Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 19:30:47 -05:00
J. Nick Koston 8af64629d7 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 18:59:47 -05:00
J. Nick Koston 6dbe414b77 Assemble plain .s without the preprocessor, allow unflagging operand-less linker flags 2026-08-23 18:59:45 -05:00
J. Nick Koston fa6e6f3d36 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 18:59:00 -05:00
J. Nick Koston 6f775ca9af Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 18:45:16 -05:00
J. Nick Koston 2e681bfb2e Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 18:45:15 -05:00
J. Nick Koston 4c4ccc4444 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 18:33:24 -05:00
J. Nick Koston 8ef6bd24d6 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 18:33:24 -05:00
J. Nick Koston d4f75e5468 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 18:26:13 -05:00
J. Nick Koston b458d6b4af Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 18:26:13 -05:00
J. Nick Koston 0e54d7d510 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 18:20:47 -05:00
J. Nick Koston d8dea887b0 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 18:20:47 -05:00
J. Nick Koston 129f50b712 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 18:15:05 -05:00
J. Nick Koston bfafb41888 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 18:15:05 -05:00
J. Nick Koston b893cee6ca Apply simplify pass: shared idedata warning, platform-keyed backend table, one owner for the cache prefix and trigger files 2026-08-23 18:07:46 -05:00
J. Nick Koston 73fd512a25 Make symbol unflags pair-aware on the link line, drop the dead KEY_FLASH_MODE 2026-08-23 18:03:15 -05:00
J. Nick Koston aaebc6e007 Cover the skipped-size-summary warning in run_compile 2026-08-23 18:00:34 -05:00
J. Nick Koston f0f6671c83 Pin that the native upload path imports no platform component package 2026-08-23 17:52:27 -05:00
J. Nick Koston ebb6ffa509 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 17:51:16 -05:00
J. Nick Koston e902d8d1fc Widen _collect_sources exclude to Collection 2026-08-23 17:51:09 -05:00
J. Nick Koston f82d8ec99b Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 17:46:54 -05:00
J. Nick Koston a15dbe750f Pin that a non-default flash mode reaches the define and the image header 2026-08-23 17:46:53 -05:00
J. Nick Koston fde0a835b6 Pin board_build.flash_mode unconditionally, dispatch native backends by toolchain, report a skipped size summary 2026-08-23 17:46:23 -05:00
J. Nick Koston 08421c9bfb Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 17:27:07 -05:00
J. Nick Koston d59f6eb46d Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 17:27:07 -05:00
J. Nick Koston 4e97a8f7f5 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 17:18:01 -05:00
J. Nick Koston 26982767bd Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 17:18:01 -05:00
J. Nick Koston c97fb7744b Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 17:12:40 -05:00
J. Nick Koston b3ad8bd15d Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 17:12:40 -05:00
J. Nick Koston 6da4a07546 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 17:08:17 -05:00
J. Nick Koston 1f44b89815 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 17:08:17 -05:00
J. Nick Koston c2edfd80eb Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 16:54:58 -05:00
J. Nick Koston e2aee2b28e Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 16:54:58 -05:00
J. Nick Koston f1b2258661 Stop the false build_src_flags warning, share the idedata error tuple, make the no-tests guard per platform 2026-08-23 16:48:36 -05:00
J. Nick Koston 3f10a8ca36 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain
# Conflicts:
#	esphome/components/esp8266/__init__.py
2026-08-23 16:43:20 -05:00
J. Nick Koston ad4c59873b Read flash mode from its real producer, share the mode set, store the scanf decision
flash_mode comes from the board_build.flash_mode option the component
already pins (validated by cv.one_of against the same BUILD_FLASH_MODES,
now living in const.py so the two sides cannot drift), retiring
KEY_FLASH_MODE from the generator. The scanf-float decision is stored in
CORE.data where it is computed instead of being a discarded local, so
KEY_SCANF_FLOAT has a producer in this PR. A trailing bare -include in
build_src_flags fails by name instead of degrading into the source dir.
2026-08-23 16:42:25 -05:00
J. Nick Koston 09d714bbef Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 16:36:12 -05:00
J. Nick Koston 436f06eb6d Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 16:36:10 -05:00
J. Nick Koston 6f0dffeb37 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 16:31:26 -05:00
J. Nick Koston 74003888a1 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 16:31:24 -05:00
J. Nick Koston 6b39cbb3e0 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 16:25:37 -05:00
J. Nick Koston 8ac04d41e4 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 16:25:35 -05:00
J. Nick Koston f1d270198c Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 16:18:01 -05:00
J. Nick Koston a95f992430 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 16:17:59 -05:00
J. Nick Koston 008555d600 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 16:12:15 -05:00
J. Nick Koston 1ec7e7b285 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 16:12:13 -05:00
J. Nick Koston ccf7da5414 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 16:10:02 -05:00
J. Nick Koston 869bc2c99a Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 16:10:00 -05:00
J. Nick Koston 78468d7172 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 16:07:02 -05:00
J. Nick Koston fc8953d9cc Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 16:07:00 -05:00
J. Nick Koston 38c5e4f997 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 15:59:15 -05:00
J. Nick Koston 612545b9a8 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 15:59:13 -05:00
J. Nick Koston 1b28c3eadb Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 15:52:45 -05:00
J. Nick Koston ee431eb723 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 15:52:43 -05:00
J. Nick Koston 1cdaddad4f Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 15:46:01 -05:00
J. Nick Koston a8e71f125a Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 15:45:59 -05:00
J. Nick Koston de4e395715 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 15:33:44 -05:00
J. Nick Koston f955c59fe4 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 15:33:42 -05:00
J. Nick Koston 5c9d73c1e2 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 15:27:53 -05:00
J. Nick Koston eea31bf571 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 15:27:51 -05:00
J. Nick Koston 63d061bc1d Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 15:09:19 -05:00
J. Nick Koston 65793b0eb7 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 15:09:18 -05:00
J. Nick Koston 21a3509634 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 15:06:58 -05:00
J. Nick Koston 91f0554a2c Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 15:06:56 -05:00
J. Nick Koston d0e27bd174 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 14:42:33 -05:00
J. Nick Koston 5d4a18f9e5 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 14:42:31 -05:00
J. Nick Koston f0ed1ac8ba Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 14:31:41 -05:00
J. Nick Koston 718157d37f Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 14:31:39 -05:00
J. Nick Koston 6ae81d49b9 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 14:17:42 -05:00
J. Nick Koston 5166e1b207 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 14:17:40 -05:00
J. Nick Koston a429721472 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 14:06:14 -05:00
J. Nick Koston a80b5a9dd0 Cover the flash-mode rejection and src-flag pass-through 2026-08-23 14:06:11 -05:00
J. Nick Koston b0b52bec81 Ungate the build_src_flags producer for the native generator
The generator now reads build_src_flags instead of hardcoding the
throw_stubs force-include, so the option must be set on both toolchains;
gated, the native build silently lost the include (+168 bytes of
libstdc++ throw strings). Verified byte-identical again.
2026-08-23 14:01:34 -05:00
J. Nick Koston 9e623309b4 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 13:57:24 -05:00
J. Nick Koston 1ccd3cdee8 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 13:57:22 -05:00
J. Nick Koston 350a2a84cc Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain
# Conflicts:
#	esphome/components/esp8266/__init__.py
2026-08-23 13:56:47 -05:00
J. Nick Koston 4cc3012d98 Read build_src_flags from its producer, share the ldscript rule, shape-check flash_mode
The throw_stubs force-include now comes from the build_src_flags option
esp8266/__init__ pins, collapsing the two spellings to one source of
truth; -include paths resolve against the source root. The ldscript
fallback consumes boards.board_ld_script instead of duplicating the
per-board rule, flash_mode is checked against its closed set before
landing unquoted in the elf2bin command, and the board-gate comment no
longer names a nonexistent validator.
2026-08-23 13:56:27 -05:00
J. Nick Koston 39f6425170 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 13:54:27 -05:00
J. Nick Koston 51384f9743 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 13:37:27 -05:00
J. Nick Koston 4e37acdc01 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 13:37:24 -05:00
J. Nick Koston 79fae3b709 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 13:19:31 -05:00
J. Nick Koston 93b0bdff2b Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 13:19:29 -05:00
J. Nick Koston b2d0af3316 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 13:18:31 -05:00
J. Nick Koston 5aa61235e4 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 13:18:29 -05:00
J. Nick Koston c92b734bb9 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 13:17:28 -05:00
J. Nick Koston b805d73c07 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 13:17:26 -05:00
J. Nick Koston 7af97902d2 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 13:12:53 -05:00
J. Nick Koston 34456bc283 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 13:12:53 -05:00
J. Nick Koston 36c5e39371 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 13:10:39 -05:00
J. Nick Koston fd61eda4a2 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 13:10:39 -05:00
J. Nick Koston 9c9ddea00c Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 12:34:10 -05:00
J. Nick Koston 33c010e1b0 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 12:34:08 -05:00
J. Nick Koston 8d0eab9111 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 12:19:24 -05:00
J. Nick Koston 9f6d66a2e7 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission
# Conflicts:
#	esphome/build_gen/arduino8266.py
#	tests/unit_tests/build_gen/test_arduino8266.py
2026-08-23 12:18:51 -05:00
J. Nick Koston 1b255b429f Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 12:05:21 -05:00
J. Nick Koston 15727692e0 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 12:05:19 -05:00
J. Nick Koston cf7c6151e7 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 11:37:10 -05:00
J. Nick Koston 23c1527cf0 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 11:37:08 -05:00
J. Nick Koston 412bc4be7f Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 10:48:54 -05:00
J. Nick Koston acc919e993 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 10:48:54 -05:00
J. Nick Koston af08a45afe Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 10:37:31 -05:00
J. Nick Koston 2e97689afb Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 10:37:29 -05:00
J. Nick Koston caf758eaed Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-23 10:33:03 -05:00
J. Nick Koston 5bdb886190 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 10:33:03 -05:00
J. Nick Koston 33aac54c8f Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain
# Conflicts:
#	esphome/components/esp32/__init__.py
2026-08-23 10:17:28 -05:00
J. Nick Koston d6df22f7fa Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-23 10:17:10 -05:00
J. Nick Koston 2a2710d752 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 23:41:58 -05:00
J. Nick Koston c0be436a07 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 23:41:58 -05:00
J. Nick Koston c7772c384e Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 23:23:43 -05:00
J. Nick Koston 7248e59ca3 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 23:23:43 -05:00
J. Nick Koston 005aedd3a6 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 23:21:08 -05:00
J. Nick Koston e25b6c4085 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 23:21:08 -05:00
J. Nick Koston 3624ff6b19 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 23:10:58 -05:00
J. Nick Koston 57b5bb1b82 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 23:10:58 -05:00
J. Nick Koston 505f4dee03 Mirror the damaged-file recovery tests into the chain 2026-08-22 23:10:37 -05:00
J. Nick Koston 1e5f7fe102 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 23:02:22 -05:00
J. Nick Koston ade226456e Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 23:02:22 -05:00
J. Nick Koston 6d3c78b5bc Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 23:00:19 -05:00
J. Nick Koston f326f7b527 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 23:00:19 -05:00
J. Nick Koston b302f4cb9a Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 22:58:49 -05:00
J. Nick Koston d37ebd5cd7 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 22:58:48 -05:00
J. Nick Koston 1543709ddd Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 22:40:59 -05:00
J. Nick Koston c2c6cf9398 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 22:40:59 -05:00
J. Nick Koston a424629ab6 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 22:39:49 -05:00
J. Nick Koston b2111b5070 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 22:39:49 -05:00
J. Nick Koston d1299430bb Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 22:32:38 -05:00
J. Nick Koston a75ea8225c Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 22:32:38 -05:00
J. Nick Koston 538942153d Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain
# Conflicts:
#	esphome/components/esp8266/__init__.py
2026-08-22 22:30:05 -05:00
J. Nick Koston cd3cdde87c Preserve d1_wroom_02's shipped flash layout, refuse plain linker unflags, pin the real spaced-override outcome 2026-08-22 22:29:21 -05:00
J. Nick Koston 013a27bee9 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 22:17:28 -05:00
J. Nick Koston 4577d69ca9 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission
# Conflicts:
#	esphome/build_gen/arduino8266.py
2026-08-22 22:17:26 -05:00
J. Nick Koston dce5185cef Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 22:11:45 -05:00
J. Nick Koston a7e5ac9dea Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 22:11:44 -05:00
J. Nick Koston 476852c84f Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission
# Conflicts:
#	esphome/build_gen/arduino8266.py
2026-08-22 22:11:14 -05:00
J. Nick Koston 9fe10af350 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 21:55:44 -05:00
J. Nick Koston a5947beb07 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 21:55:44 -05:00
J. Nick Koston 2fe3eaf97d Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 21:50:50 -05:00
J. Nick Koston 2bfbc72147 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission
# Conflicts:
#	esphome/build_gen/arduino8266.py
2026-08-22 21:50:38 -05:00
J. Nick Koston aadc10944c Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission
# Conflicts:
#	esphome/build_gen/arduino8266.py
2026-08-22 21:49:06 -05:00
J. Nick Koston 24ea73a626 Gate the zero-test guard behind a flag, keep the upload path import-free, verify all artifacts, regenerate a stale compile DB 2026-08-22 21:47:03 -05:00
J. Nick Koston 9d4df93676 Assert the launcher string verbatim; Path normalization diverged on Windows 2026-08-22 21:42:53 -05:00
J. Nick Koston 390ca116bc Name the idedata consumers in the arduino backend's warning too 2026-08-22 21:42:15 -05:00
J. Nick Koston 2d325a9eee Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 21:42:14 -05:00
J. Nick Koston fbe5a02d6f Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 21:42:14 -05:00
J. Nick Koston beceab6327 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 21:30:41 -05:00
J. Nick Koston 822bb1d88f Drop the stale ccache_path patch from the write_project test 2026-08-22 21:29:50 -05:00
J. Nick Koston 9487cf6b7c Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 21:29:33 -05:00
J. Nick Koston a003fb848a Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 21:24:08 -05:00
J. Nick Koston e465839d17 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 21:24:08 -05:00
J. Nick Koston d43770b6d4 Drop a stale ccache_path patch from the write_project test 2026-08-22 21:22:41 -05:00
J. Nick Koston cc73c0c67e Dispatch esptool flashing via the backend hook, share the native trigger files, gate the probe, cache the closure 2026-08-22 21:21:25 -05:00
J. Nick Koston ff84e53ff1 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 21:19:00 -05:00
J. Nick Koston 0f3ac3b0e3 Point the board-check comment at the validator that now exists 2026-08-22 21:18:58 -05:00
J. Nick Koston aa63eb9734 Move the resolve-for-me ccache sentinel to its one consumer 2026-08-22 21:18:48 -05:00
J. Nick Koston 9144ae462d Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 21:17:55 -05:00
J. Nick Koston aa5700d150 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 21:17:54 -05:00
J. Nick Koston 64d0137ab8 Verify build artifacts and surface probe diagnostics; fail on a zero-test CI run 2026-08-22 20:49:02 -05:00
J. Nick Koston 53cfdc92ba Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 20:26:38 -05:00
J. Nick Koston 13ae59378d Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 20:26:38 -05:00
J. Nick Koston 72b37a0bf2 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 20:13:48 -05:00
J. Nick Koston 378d5f53e3 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 20:13:48 -05:00
J. Nick Koston 7460f8df1a Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 20:01:32 -05:00
J. Nick Koston eeb26a375f Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 20:01:32 -05:00
J. Nick Koston 4fa3ad4381 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 19:55:34 -05:00
J. Nick Koston 19bb36c7ff Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 19:55:34 -05:00
J. Nick Koston a80db169a4 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 19:46:47 -05:00
J. Nick Koston 93ce8ac58e Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 19:46:46 -05:00
J. Nick Koston cf5fd1f36a Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 19:32:13 -05:00
J. Nick Koston 84376f0da6 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 19:32:13 -05:00
J. Nick Koston 4aafc96ef7 Pin the analyze-memory ELF guard in tests 2026-08-22 19:18:13 -05:00
J. Nick Koston 2af06e3b53 Use the exported consumed-options set, guard the analyze-memory ELF, trigger on the cache action 2026-08-22 19:16:50 -05:00
J. Nick Koston c26f77fe62 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 19:15:09 -05:00
J. Nick Koston c3eeb3fa2a Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission
# Conflicts:
#	tests/unit_tests/build_gen/test_arduino8266.py
2026-08-22 19:15:07 -05:00
J. Nick Koston b5219d3c60 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 17:24:38 -05:00
J. Nick Koston 050aa1ea21 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 17:24:38 -05:00
J. Nick Koston 7e8b033860 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 17:10:24 -05:00
J. Nick Koston a7fe085e87 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 17:10:24 -05:00
J. Nick Koston aaf27d0e90 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 16:59:09 -05:00
J. Nick Koston a143d192c5 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 16:59:09 -05:00
J. Nick Koston b5fbb767c2 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 16:41:54 -05:00
J. Nick Koston e9c2363a38 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 16:41:53 -05:00
J. Nick Koston e3d731b1df Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 16:38:32 -05:00
J. Nick Koston 39d18e7411 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 16:38:31 -05:00
J. Nick Koston 53fb66616c Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 16:16:23 -05:00
J. Nick Koston 0ace5d0415 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 16:16:23 -05:00
J. Nick Koston 3219f02e23 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain
# Conflicts:
#	esphome/__main__.py
2026-08-22 16:10:47 -05:00
J. Nick Koston b024fd53dd Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 16:10:10 -05:00
J. Nick Koston 5681890f74 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 15:58:12 -05:00
J. Nick Koston 7d51911102 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 15:58:11 -05:00
J. Nick Koston 5ae3009791 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 15:51:43 -05:00
J. Nick Koston 3f006545c1 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 15:51:42 -05:00
J. Nick Koston a97479105f Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 15:47:13 -05:00
J. Nick Koston a2e4664e87 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 15:47:12 -05:00
J. Nick Koston 3635c05fa5 Non-destructive platform_version check, honest compdb errors, missing-binutils refusal
The platform_version pop defaults to the schema spec so a second
validation pass over an already-validated dict cannot warn about a key
the user never set. An unparsable compile database now fails naming the
parse error and the offending output instead of blaming renamed ninja
rules. analyze-memory validates the native objdump/readelf exist and
fails by tool name rather than silently analyzing with host binutils.
The shared smoke-test helper is renamed _toolchain_components_to_test
(it serves the esp32 PlatformIO job too), and the decode-dedup state is
cleared by an autouse fixture instead of by hand.
2026-08-22 15:36:49 -05:00
J. Nick Koston 51ae50cb27 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 15:33:41 -05:00
J. Nick Koston 160299a41e Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 15:33:41 -05:00
J. Nick Koston 0910604166 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 15:25:48 -05:00
J. Nick Koston 3641621b0e Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 15:25:48 -05:00
J. Nick Koston e8dea9b07f Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 15:10:17 -05:00
J. Nick Koston 2e570bdc02 Correct the board-check comment; note the override behavior of get_flash_ld_path 2026-08-22 15:10:14 -05:00
J. Nick Koston e00229d22c Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 15:06:32 -05:00
J. Nick Koston 7c47cbdb73 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission
# Conflicts:
#	esphome/build_gen/arduino8266.py
2026-08-22 15:06:30 -05:00
J. Nick Koston ae38e587fa Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 15:03:22 -05:00
J. Nick Koston b29fbe5839 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 15:03:21 -05:00
J. Nick Koston 298c78ff09 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 14:54:58 -05:00
J. Nick Koston 4ebdd88d7b Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 14:54:58 -05:00
J. Nick Koston 2c6cbdef4d Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 14:52:41 -05:00
J. Nick Koston 9f64d54e50 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 14:52:40 -05:00
J. Nick Koston 4d9da7284e Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 14:44:18 -05:00
J. Nick Koston fd82f0f4bf Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 14:44:18 -05:00
J. Nick Koston 28107bf966 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 14:41:35 -05:00
J. Nick Koston 594cc89b38 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 14:41:34 -05:00
J. Nick Koston 23bc88355b Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 14:38:09 -05:00
J. Nick Koston 342b61ef84 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 14:38:09 -05:00
J. Nick Koston 42134329e3 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 14:31:40 -05:00
J. Nick Koston f9f642b81d Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 14:31:39 -05:00
J. Nick Koston 57248d3d6f Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 14:21:11 -05:00
J. Nick Koston 007cd61fe4 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 14:21:11 -05:00
J. Nick Koston 1d88a5a92b Keep no-op rebuilds quiet
A -n dry-run probe answers 'no work to do' without the real ninja spawn,
and running with cwd instead of -C drops the 'Entering directory' banner
on real builds too.
2026-08-22 14:14:37 -05:00
J. Nick Koston 3985ca5d51 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 14:12:57 -05:00
J. Nick Koston 0e1c7cbb7e Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 14:12:57 -05:00
J. Nick Koston eb62957b66 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 13:51:48 -05:00
J. Nick Koston 7fe793da17 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 13:51:48 -05:00
J. Nick Koston 255ca06325 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 13:16:00 -05:00
J. Nick Koston 893ec5d3f5 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 13:16:00 -05:00
J. Nick Koston e8ceb241e1 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 13:10:55 -05:00
J. Nick Koston c8dd9dd483 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 13:10:55 -05:00
J. Nick Koston ac227b552c Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 12:52:53 -05:00
J. Nick Koston efe2dc481d Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 12:52:53 -05:00
J. Nick Koston fe357106df Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 12:48:21 -05:00
J. Nick Koston 751914c47f Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 12:48:21 -05:00
J. Nick Koston 43411106bb Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 12:26:39 -05:00
J. Nick Koston e107747643 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 12:26:39 -05:00
J. Nick Koston e41440b16b Tilde-expand the install prefix before the CI cache-path assertion
The job env carries a literal ~ that Python's expanduser and
actions/cache both resolve; the assertion compared the raw string and
failed the native toolchain job.
2026-08-22 12:23:24 -05:00
J. Nick Koston 19d8f832b9 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain
# Conflicts:
#	esphome/components/esp8266/__init__.py
2026-08-22 12:16:27 -05:00
J. Nick Koston d7edb52c7b Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 12:15:40 -05:00
J. Nick Koston 8b7d199ff9 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 12:08:57 -05:00
J. Nick Koston e7aa269346 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 12:08:57 -05:00
J. Nick Koston b002a37987 Fail fast on empty compile databases and unsupported analyze-memory toolchains; degrade a missing size tool
ninja -t compdb exits 0 with an empty list when the rule names drift, so
the compile database is now parsed and an empty result fails the build
and drops the stale file. analyze-memory refuses an unsupported
toolchain before paying for the compile. A missing size binary warns
instead of discarding an already-linked build, _parse_app_size returns
explicitly from its not-found branch, and the CI cache action asserts
the caller's install prefix matches the cached path. The esp8266
run_compile dispatch hook and copy_files early-return gain direct
tests.
2026-08-22 12:06:38 -05:00
J. Nick Koston 583f47034f Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 12:02:36 -05:00
J. Nick Koston 10a22d7d74 Quote an overridden ldscript name on the link line; note the deliberate board-derived --flash_size
The -T script name was the one user-controlled token on the link line
that skipped shell_token, so a space or dollar sign in a
board_build.ldscript override corrupted the emitted linkflags. The
elf2bin comment now records that --flash_size deliberately stays
board-derived, matching PlatformIO (which reads upload.maximum_size,
not the ldscript).
2026-08-22 12:02:33 -05:00
J. Nick Koston 905b056756 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission
# Conflicts:
#	esphome/build_gen/arduino8266.py
2026-08-22 12:01:44 -05:00
J. Nick Koston b597a4f02f Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 11:55:59 -05:00
J. Nick Koston 0c64ce7a8c Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 11:55:59 -05:00
J. Nick Koston fcc7bb8c44 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 11:49:37 -05:00
J. Nick Koston 3e646470ef Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 11:49:36 -05:00
J. Nick Koston 4866ef97f0 Trim comment essays and hoist function-local test imports 2026-08-22 11:46:49 -05:00
J. Nick Koston 7e675233c6 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 11:44:16 -05:00
J. Nick Koston 512a31b3fc Trim comment essays and hoist function-local test imports 2026-08-22 11:44:09 -05:00
J. Nick Koston 3134239d52 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 11:43:02 -05:00
J. Nick Koston 9304c74de5 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 11:24:58 -05:00
J. Nick Koston 538d52c6bc Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 11:24:58 -05:00
J. Nick Koston 266f67ef13 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 11:04:48 -05:00
J. Nick Koston ee218bdc7d Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 11:04:47 -05:00
J. Nick Koston 772fb65675 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 10:03:46 -05:00
J. Nick Koston 116f7479a3 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 10:03:46 -05:00
J. Nick Koston 00549813ab Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 09:41:49 -05:00
J. Nick Koston c2e920d769 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 09:41:49 -05:00
J. Nick Koston ccce5e9ab2 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 09:30:02 -05:00
J. Nick Koston 3b9a026fd4 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 09:30:02 -05:00
J. Nick Koston 685409cf83 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 09:21:11 -05:00
J. Nick Koston bff4b7d870 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 09:21:11 -05:00
J. Nick Koston 7c91407835 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 00:48:41 -05:00
J. Nick Koston 33c6b3161b Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 00:48:41 -05:00
J. Nick Koston 76a80bd346 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-22 00:18:46 -05:00
J. Nick Koston 7ab66de38b Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-22 00:18:46 -05:00
J. Nick Koston 0f885b4037 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 21:27:59 -05:00
J. Nick Koston 09518f4032 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 21:27:59 -05:00
J. Nick Koston 8b21fa620d Cover the pre-resolved ccache branch of get_idedata 2026-08-21 21:20:58 -05:00
J. Nick Koston 116b4ce444 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 21:11:22 -05:00
J. Nick Koston 91cf9be5eb Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 21:11:21 -05:00
J. Nick Koston eecdf3e68d Drop the components-graph memo; it defeats the tested disk-cache semantics
The in-process cache served a stale graph across mtime and version
changes, which the script test suite pins; the saved re-parse was tens
of milliseconds.
2026-08-21 21:11:11 -05:00
J. Nick Koston c0a605c1b9 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 21:01:48 -05:00
J. Nick Koston a9994c6895 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 21:01:47 -05:00
J. Nick Koston 9fe9d8da1b Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 20:56:38 -05:00
J. Nick Koston 897713cbcb Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 20:56:38 -05:00
J. Nick Koston 8b9f5503df Drop the now-inlined pio_options module 2026-08-21 20:55:58 -05:00
J. Nick Koston 17f718e8c9 Simplify the native dispatch and CI cache wiring
A cache-arduino8266 composite action mirrors cache-esp-idf (one key
resolver, dev-writes/PR-restores) and both jobs pin
ESPHOME_ARDUINO8266_PREFIX so YAML and Python agree on the path by
construction. esp32 provides the native_toolchain_module hook, so the
shared dispatcher never names a backend and the unclaimed-native raise
loses its esp-idf carve-out; the esp8266 decode and run_compile hooks
resolve through the same seam. run_compile resolves ccache once and
threads it to the generator, env, and idedata; the compdb rule names
follow the shared kinds; the pio-options warner inlines into the driver
and derives its consumed set from the core routing constant; the
per-toolchain CI narrowing shares one body; create_components_graph is
memoized per run; the decode rate-limit drops its math sentinel.
2026-08-21 20:55:27 -05:00
J. Nick Koston 39f11815ed Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 20:55:19 -05:00
J. Nick Koston 16bc83032d Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 20:55:19 -05:00
J. Nick Koston ff0298c66a Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 20:51:57 -05:00
J. Nick Koston 814109df47 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 20:51:57 -05:00
J. Nick Koston 80a4178f05 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 20:49:32 -05:00
J. Nick Koston 0973044214 Strip the python path for cmd.exe, name rules by kind, take the resolved install
The build.ninja python variable goes through strip_win_long_path_prefix
like every other emitted binary path (the NSIS launcher's extended
prefix cannot be spawned by cmd.exe). Compile rules are named by the
shared source kinds directly, retiring the identity translation table
and its drift test. write_project takes the caller's resolved ccache,
get_flash_ld_path reads the InstalledPaths the ninja file linked
against instead of re-resolving the version, the board backstop checks
the one table it indexes with an honest comment, and cc/cxx/ar paths go
through the shared toolchain_tool accessor (gaining the Windows
suffix).
2026-08-21 20:49:30 -05:00
J. Nick Koston d628bc3aae Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission
# Conflicts:
#	esphome/build_gen/arduino8266.py
#	tests/unit_tests/build_gen/test_arduino8266.py
2026-08-21 20:47:42 -05:00
J. Nick Koston 9268f52b9e Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 20:38:33 -05:00
J. Nick Koston df91523ce3 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 20:38:33 -05:00
J. Nick Koston f2a878cb25 Stop warning about the now-honored f_cpu and ldscript overrides 2026-08-21 20:31:12 -05:00
J. Nick Koston c9a40e550c Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 20:30:32 -05:00
J. Nick Koston 5474ba2ca1 Honor board_build.ldscript for the native flash linker script
Published configs override it to reserve a filesystem region or correct
a board's assumed flash size; the routed value replaces the BOARDS
default, and a path is rejected since the name resolves via -L.
2026-08-21 20:30:30 -05:00
J. Nick Koston 6070941771 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 20:22:05 -05:00
J. Nick Koston d78083f9f2 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 20:22:05 -05:00
J. Nick Koston dc0fddd349 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 19:59:57 -05:00
J. Nick Koston 44db267a2c Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission
# Conflicts:
#	esphome/build_gen/arduino8266.py
#	tests/unit_tests/build_gen/test_arduino8266.py
2026-08-21 19:59:54 -05:00
J. Nick Koston 26ea560139 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 19:24:21 -05:00
J. Nick Koston 4cec2e119c Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 19:24:20 -05:00
J. Nick Koston f3395afae1 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 18:56:03 -05:00
J. Nick Koston 69f558d83d Pin the board-table equality, note the flash_freq audit
write_project rejects a board missing from either table, so a new test
asserts the two stay exactly equal (the subset check is how d1_wroom_02
went missing). The hardcoded --flash_freq 40 carries the audit note:
all 45 supported boards ship 40 MHz, and a platform bump adding one
that does not must revisit it.
2026-08-21 18:55:52 -05:00
J. Nick Koston 6b56def2bf Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission
# Conflicts:
#	esphome/build_gen/arduino8266.py
#	tests/unit_tests/build_gen/test_arduino8266.py
2026-08-21 18:55:40 -05:00
J. Nick Koston f451de062a Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 17:45:05 -05:00
J. Nick Koston 923e293537 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 17:45:05 -05:00
J. Nick Koston 88e6471a4b Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 17:38:34 -05:00
J. Nick Koston 9fd323e7d9 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 17:38:33 -05:00
J. Nick Koston daea0cc9f3 Warn on missing decode tools on both toolchains, fail the CI key resolver loudly
The PlatformIO branch of _decode_pc now routes through the same
rate-limited warning as the native one; raw undecoded crash addresses
with no stated reason were undiagnosable at default log level. The CI
cache key uses the assignment form so errexit catches a resolver
failure instead of echo swallowing it into a degenerate key, with a
non-empty check, and the cache comments stop claiming a ccache store
the seed job never populates (it saves before any compile runs).
2026-08-21 17:26:11 -05:00
J. Nick Koston cb409e276a Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 17:19:09 -05:00
J. Nick Koston 978d7fc2e7 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 17:19:09 -05:00
J. Nick Koston 27b80f895b Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 17:04:13 -05:00
J. Nick Koston e2cc4e825f Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 17:04:12 -05:00
J. Nick Koston 3e56619cd6 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 16:52:50 -05:00
J. Nick Koston 84d98a3593 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 16:52:50 -05:00
J. Nick Koston 7a99e65853 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 16:07:41 -05:00
J. Nick Koston 5622a7b851 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 16:07:40 -05:00
J. Nick Koston 0127b483b0 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 15:34:01 -05:00
J. Nick Koston 114302b518 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 15:34:01 -05:00
J. Nick Koston 98f574c251 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 15:19:11 -05:00
J. Nick Koston 568885f80d Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 15:19:11 -05:00
J. Nick Koston dbae5246e6 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 14:50:47 -05:00
J. Nick Koston 2fba991b51 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 14:50:47 -05:00
J. Nick Koston b131f51788 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 14:32:37 -05:00
J. Nick Koston ccaa2a8f0d Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 14:32:37 -05:00
J. Nick Koston 0097ce892c Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 14:12:34 -05:00
J. Nick Koston a3b2954321 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 14:12:33 -05:00
J. Nick Koston 9fbc996d52 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 13:33:19 -05:00
J. Nick Koston 046c7e5eb0 Lex build flags once in write_project, warn on unflags that match nothing
The pre-lexed tokens feed both _flag_defines and _project_flags so a
malformed entry warns once per generation. An unflag that hits nothing
in the user flags or any framework set (a typo, or -DUSE_FOO against
-DUSE_FOO=1) is named instead of silently leaving the flag in force.
2026-08-21 13:33:11 -05:00
J. Nick Koston c0cd64cd76 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 13:32:21 -05:00
J. Nick Koston c92b3cc49a Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 13:13:16 -05:00
J. Nick Koston 82d1ffebf2 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 13:13:15 -05:00
J. Nick Koston c3c27dc744 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 13:07:04 -05:00
J. Nick Koston 9153f03c83 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission
# Conflicts:
#	tests/unit_tests/build_gen/test_arduino8266.py
2026-08-21 13:07:03 -05:00
J. Nick Koston 6974a67626 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 12:57:46 -05:00
J. Nick Koston e9409f46db Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 12:57:45 -05:00
J. Nick Koston debfff005b Check the elf2bin inputs at generation, quote cxx flags, add d1_wroom_02 to BOARDS
elf2bin.py and eboot.elf join the existence checks so a half-extracted
package fails by name before the compile instead of at the
second-to-last edge. get_project_cxx_compile_flags() values go through
_shell_token like every other flag source. d1_wroom_02 (2 MB, in
ESP8266_BOARD_BUILD and buildable under PlatformIO) joins BOARDS so the
native board guard stops rejecting it, and the guard comment states
what config validation actually checks.
2026-08-21 12:57:36 -05:00
J. Nick Koston 523150a4c9 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission
# Conflicts:
#	tests/unit_tests/build_gen/test_arduino8266.py
2026-08-21 12:55:51 -05:00
J. Nick Koston 4936ec0de6 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 12:28:19 -05:00
J. Nick Koston afe12061d2 Track the per-platform shell quoting in the emission drift tests
The asm -D/-I filter also strips a leading single quote now that POSIX
tokens are single-quoted.
2026-08-21 12:28:11 -05:00
J. Nick Koston 2d89ca3dd0 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission
# Conflicts:
#	tests/unit_tests/build_gen/test_arduino8266.py
2026-08-21 12:26:46 -05:00
J. Nick Koston f12cb4350a Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 12:25:14 -05:00
J. Nick Koston 3e218e43cd Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 12:25:14 -05:00
J. Nick Koston c8722421b1 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 11:14:36 -05:00
J. Nick Koston 6093f3f1ce Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 11:14:35 -05:00
J. Nick Koston 9ff0198865 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 09:58:45 -05:00
J. Nick Koston 2ee1d89e04 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 09:58:44 -05:00
J. Nick Koston 503e3ca724 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-21 09:36:01 -05:00
J. Nick Koston 3a49955b45 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 09:36:00 -05:00
J. Nick Koston e5cfb1bdac Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain
# Conflicts:
#	tests/unit_tests/test_main.py
2026-08-21 01:06:06 -05:00
J. Nick Koston 71c2381c60 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-21 01:05:51 -05:00
J. Nick Koston aae5538fd5 Gate uploads on a built firmware, refuse unclaimed native builds, seed the CI toolchain cache from dev
The upload path now checks CORE.firmware_bin exists before invoking
esptool and names the fix (compile first) instead of failing inside
esptool. compile_program raises when a native toolchain resolved but no
platform backend claimed the build rather than falling through to a
mis-configured PlatformIO project. The addr2line failure warning now
includes the exception so the cause is visible without debug logging.

CI: the native toolchain cache is restore-only on PRs and seeded by a
new dev-push job, mirroring seed-apt-cache; the key resolves the pinned
core and toolchain versions from code instead of hashing framework.py.
determine-jobs main() output tests now pin the esp8266_native pair.
2026-08-21 00:05:23 -05:00
J. Nick Koston 01923ff22a Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 23:58:42 -05:00
J. Nick Koston 5a5a379444 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission
# Conflicts:
#	tests/unit_tests/build_gen/test_arduino8266.py
2026-08-20 23:58:18 -05:00
J. Nick Koston f8d3df8196 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain
# Conflicts:
#	tests/unit_tests/test_main.py
2026-08-20 23:48:20 -05:00
J. Nick Koston 21dfa0846e Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 23:47:51 -05:00
J. Nick Koston 5311be5c91 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 22:08:44 -05:00
J. Nick Koston 553adf6d24 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 22:08:44 -05:00
J. Nick Koston 6e56d19615 Mirror the LookupError tolerance in the native post-build guard 2026-08-20 21:09:56 -05:00
J. Nick Koston 3e21c35e3e Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 21:09:44 -05:00
J. Nick Koston 5b42eb3460 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 21:09:44 -05:00
J. Nick Koston 58911dc8bc Cover the missing-hook raise and the analyze-memory refusal 2026-08-20 21:01:14 -05:00
J. Nick Koston a242624ea6 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 20:53:32 -05:00
J. Nick Koston a17300a4bb Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 20:53:32 -05:00
J. Nick Koston f0e3fb48c3 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 20:48:33 -05:00
J. Nick Koston f9d71398d5 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 20:48:33 -05:00
J. Nick Koston d22a941b24 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 20:46:31 -05:00
J. Nick Koston a997b306ea Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 20:46:31 -05:00
J. Nick Koston 1ab13c4291 Cache the native toolchain install and ccache store in CI 2026-08-20 20:45:02 -05:00
J. Nick Koston 1d6a62a8a5 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 20:44:26 -05:00
J. Nick Koston ee9045f021 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 20:44:26 -05:00
J. Nick Koston 3f96590567 Backend-agnostic dispatch guards, positive native polarity, warn on dropped upload_speed 2026-08-20 20:44:08 -05:00
J. Nick Koston 77e9a1d991 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 20:42:44 -05:00
J. Nick Koston fe5325b007 Honor lib_archive, guard unknown boards by name, quote archive names, fix the asm flag filter 2026-08-20 20:42:23 -05:00
J. Nick Koston 84a10031ee Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 20:41:36 -05:00
J. Nick Koston cbdfb37470 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 19:51:31 -05:00
J. Nick Koston 48c5570784 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 19:51:31 -05:00
J. Nick Koston 339f0c3092 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 19:29:29 -05:00
J. Nick Koston fc26bc081b Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 19:29:29 -05:00
J. Nick Koston 7aaa874150 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain
# Conflicts:
#	tests/unit_tests/test_main.py
2026-08-20 19:26:19 -05:00
J. Nick Koston 3466285547 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 19:25:46 -05:00
J. Nick Koston c9dbf79970 Widen the post-build idedata guard the same way 2026-08-20 19:03:14 -05:00
J. Nick Koston 6825996e32 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 19:02:47 -05:00
J. Nick Koston 5bdd387c2a Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 19:02:47 -05:00
J. Nick Koston 406d5c87ed Warn instead of failing when the post-build idedata is unusable 2026-08-20 18:41:38 -05:00
J. Nick Koston 8e63fa3c2a Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain
# Conflicts:
#	tests/unit_tests/test_main.py
2026-08-20 18:41:07 -05:00
J. Nick Koston 604ceb3de3 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 18:40:47 -05:00
J. Nick Koston 0186a0568b Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 18:28:28 -05:00
J. Nick Koston d342d0a246 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 18:28:28 -05:00
J. Nick Koston 294fc2dd54 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 18:18:29 -05:00
J. Nick Koston 0ae75b01d8 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 18:18:29 -05:00
J. Nick Koston 949a45b29d Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 17:54:09 -05:00
J. Nick Koston 6437a676b8 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 17:54:09 -05:00
J. Nick Koston 63da361100 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 17:30:37 -05:00
J. Nick Koston cff6871bd7 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 17:30:37 -05:00
J. Nick Koston ee27ba64c0 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 17:12:52 -05:00
J. Nick Koston 53c07a2bb5 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 17:12:52 -05:00
J. Nick Koston badf21e7b0 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 16:56:37 -05:00
J. Nick Koston a59b0bfcf8 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 16:56:37 -05:00
J. Nick Koston ab81ae8aec Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 16:55:09 -05:00
J. Nick Koston 6aec54d8c9 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 16:55:09 -05:00
J. Nick Koston ab7faccf2c Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 16:51:31 -05:00
J. Nick Koston b3927e0845 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 16:51:30 -05:00
J. Nick Koston ba569e72ec Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 16:46:52 -05:00
J. Nick Koston bb5c0ff930 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 16:46:52 -05:00
J. Nick Koston 71fffcf410 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 16:34:32 -05:00
J. Nick Koston b82cc8be65 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 16:34:32 -05:00
J. Nick Koston 301e7efad5 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 16:30:27 -05:00
J. Nick Koston 1bc9312ed7 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 16:30:27 -05:00
J. Nick Koston 5d10240521 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 16:25:49 -05:00
J. Nick Koston 137f58f00e Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 16:25:49 -05:00
J. Nick Koston baedceb8b6 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 16:24:56 -05:00
J. Nick Koston 5f09bf868d Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 16:24:56 -05:00
J. Nick Koston f747322bc0 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 16:21:04 -05:00
J. Nick Koston 6eaa43a7b2 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission
# Conflicts:
#	tests/unit_tests/build_gen/test_arduino8266.py
2026-08-20 16:21:03 -05:00
J. Nick Koston 2cb5e8d818 Warn on a failing addr2line on the PlatformIO path too 2026-08-20 16:15:42 -05:00
J. Nick Koston 0f40f23ec4 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 16:15:16 -05:00
J. Nick Koston 5c36fefe21 Drop the escaping test duplicated by the shared ninja module tests 2026-08-20 16:15:15 -05:00
J. Nick Koston e12bad27bb Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 16:14:26 -05:00
J. Nick Koston c3f7ce7a02 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission
# Conflicts:
#	tests/unit_tests/build_gen/test_arduino8266.py
2026-08-20 16:14:24 -05:00
J. Nick Koston 252422c706 Match ASPPCOM flag routing, quote manifest link tokens, split the src-dir error, pin the change contract 2026-08-20 16:12:28 -05:00
J. Nick Koston c629e4a94c Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission
# Conflicts:
#	esphome/build_gen/arduino8266.py
#	tests/unit_tests/build_gen/test_arduino8266.py
2026-08-20 16:07:57 -05:00
J. Nick Koston 15f21342d0 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 15:55:02 -05:00
J. Nick Koston 1aece78707 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 15:55:02 -05:00
J. Nick Koston ed48f36fa8 Route native dispatch through a platform hook, hoist the pio-options warner, adopt shared toolchain factories 2026-08-20 15:53:58 -05:00
J. Nick Koston b3a0856afd Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 15:49:05 -05:00
J. Nick Koston aa4dd3d0a1 Hoist the ninja quoting helpers, type the installed paths, format flash size from board data 2026-08-20 15:48:56 -05:00
J. Nick Koston 8f40b00fc3 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 15:46:56 -05:00
J. Nick Koston cbd5e08aa9 Adopt the shared size line printer and cover the moved modules in the native CI triggers 2026-08-20 15:30:03 -05:00
J. Nick Koston d736b0d1eb Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 15:29:04 -05:00
J. Nick Koston ea429ffdb6 Derive the suffix rule map, dedupe quoting and unflag filtering, adopt the moved library backend 2026-08-20 15:28:55 -05:00
J. Nick Koston a46a8f4832 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 15:27:10 -05:00
J. Nick Koston 8043767460 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 15:13:09 -05:00
J. Nick Koston e981e6014d Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 15:13:08 -05:00
J. Nick Koston 1c7b4275ad Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 15:12:49 -05:00
J. Nick Koston 7dca5da948 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 15:12:49 -05:00
J. Nick Koston 5d173d851e Cover the idedata failure warning after a successful build 2026-08-20 14:53:58 -05:00
J. Nick Koston 2c883a2c26 Toolchain-suffixed idedata cache, surfaced idedata failures, and per-dump decode warnings 2026-08-20 14:48:18 -05:00
J. Nick Koston 258417d7d3 Merge branch 'esp8266-native-library-backend' into esp8266-arduino-toolchain 2026-08-20 14:47:14 -05:00
J. Nick Koston d6a9fd5238 Adopt the shared toolchain check, dedupe the native-module dispatch, and complete the trigger set 2026-08-20 14:32:31 -05:00
J. Nick Koston 421f648a07 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 14:30:15 -05:00
J. Nick Koston a0d56d02d0 Tighten the emission half: shared srcflags, single escapes, one testing-name derivation 2026-08-20 14:29:54 -05:00
J. Nick Koston b384483fdc Merge branch 'esp8266-native-framework-installer' into esp8266-arduino-toolchain 2026-08-20 14:24:43 -05:00
J. Nick Koston d330d6ff99 Merge branch 'esp8266-native-framework-installer' into esp8266-native-ninja-emission 2026-08-20 14:24:42 -05:00
J. Nick Koston 0f56a37242 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 14:22:17 -05:00
J. Nick Koston 866c59384c Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 14:22:05 -05:00
J. Nick Koston e77102e9a8 Merge branch 'esp8266-native-framework-installer' into esp8266-arduino-toolchain 2026-08-20 14:07:49 -05:00
J. Nick Koston 83d9351925 Merge branch 'esp8266-native-framework-installer' into esp8266-native-ninja-emission 2026-08-20 14:07:48 -05:00
J. Nick Koston 5aecdd9c7a Point the ccache tests at the shared helpers 2026-08-20 14:06:43 -05:00
J. Nick Koston b584085af2 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 14:05:09 -05:00
J. Nick Koston d4c4424587 Merge branch 'esp8266-native-framework-installer' into esp8266-native-ninja-emission 2026-08-20 14:04:54 -05:00
J. Nick Koston 3f70f11b71 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 13:50:10 -05:00
J. Nick Koston 73132ba852 Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission 2026-08-20 13:49:45 -05:00
J. Nick Koston aa3c15d098 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 13:37:23 -05:00
J. Nick Koston d3524db735 Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 13:35:53 -05:00
J. Nick Koston 340f098872 Merge branch 'esp8266-native-shared-helpers' into esp8266-native-ninja-emission 2026-08-20 13:35:43 -05:00
J. Nick Koston 5993a5acfa Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain 2026-08-20 13:04:59 -05:00
J. Nick Koston 207968f301 Add the ESP8266 native ninja emission 2026-08-20 13:03:50 -05:00
J. Nick Koston 9ddcd6336e Merge branch 'esp8266-native-shared-helpers' into esp8266-arduino-toolchain 2026-08-20 12:51:01 -05:00
J. Nick Koston 898bb78c7b Merge branch 'esp8266-native-shared-helpers' into esp8266-arduino-toolchain 2026-08-20 12:48:03 -05:00
J. Nick Koston c96360d83c Merge branch 'esp8266-native-shared-helpers' into esp8266-arduino-toolchain 2026-08-20 12:46:43 -05:00
J. Nick Koston 96c1b90f19 Merge remote-tracking branch 'origin/dev' into esp8266-arduino-toolchain 2026-08-20 12:32:30 -05:00
J. Nick Koston 100dc04b38 Re-glue spaced -D tokens so knob defines are detected like PlatformIO 2026-08-20 11:37:28 -05:00
J. Nick Koston 7a2eaad9ce Warn when a compile entry does not lead with a compiler and fail on an empty core archive 2026-08-20 10:48:58 -05:00
J. Nick Koston 785260626e Warn once per missing decode tool and document the launcher-free ESP-IDF compile database 2026-08-20 10:45:36 -05:00
J. Nick Koston 1477de24d3 Skip the RAM and Flash summary lines together when the flash budget is unknown 2026-08-20 10:09:57 -05:00
J. Nick Koston 069ae156f9 Strip the exact configured launcher from compile commands instead of guessing by name 2026-08-20 09:55:15 -05:00
J. Nick Koston 0d38e47646 Share the argv quoting core between path and token quoting 2026-08-20 09:42:49 -05:00
J. Nick Koston d614e91753 Cross-platform token quoting, ninja-safe dollar escaping, and a tighter install path 2026-08-20 09:38:44 -05:00
J. Nick Koston a90dd88aa8 Lex every build_flags entry with re-quoted emission and lock the shared cache install 2026-08-20 09:33:49 -05:00
J. Nick Koston 47564d3a74 Merge branch 'dev' into esp8266-arduino-toolchain 2026-08-20 09:06:15 -05:00
J. Nick Koston 4eeceded0d Make the testing-mode require explicit, reject a missing declared srcDir, and validate installs before marking success 2026-08-20 08:08:29 -05:00
J. Nick Koston 0defff2c81 Warn when component-added platformio options are dropped by the native build 2026-08-20 07:53:16 -05:00
J. Nick Koston 0e561d8c1f Surface malformed build flag entries as clean errors 2026-08-20 07:47:54 -05:00
J. Nick Koston 2d93300903 Detect knobs in multi-token build_flags entries and drop a zero flash budget with a warning 2026-08-20 07:39:14 -05:00
J. Nick Koston a005008cfb Retry registry queries and surface failures as clean errors 2026-08-20 07:34:41 -05:00
J. Nick Koston dad9a1175b Reject unsupported toolchains on the PlatformIO-only platforms and warn on empty srcFilter matches 2026-08-20 06:52:54 -05:00
J. Nick Koston 42e9448a50 Cover the YAML toolchain key resolution 2026-08-20 06:48:24 -05:00
J. Nick Koston eaae2e543b Reject unsupported CLI toolchains on every platform and complete the native CI triggers 2026-08-20 06:30:07 -05:00
J. Nick Koston 4520f27d94 Raise the actionable ninja message when the package itself is missing 2026-08-20 06:17:10 -05:00
J. Nick Koston 3f9568eaf0 Fail loudly on hosts with no toolchain build instead of assuming linux_x86_64 2026-08-20 06:02:20 -05:00
J. Nick Koston aa03404319 Close the native CI trigger gap and log the remaining silent library skips 2026-08-20 05:35:17 -05:00
J. Nick Koston 2483117648 Store the native version floor as a Version and tidy the dependency skip 2026-08-20 05:25:25 -05:00
J. Nick Koston ff7161d7eb Surface silently dropped inputs and anchor the platform defaults to shared constants 2026-08-20 05:21:41 -05:00
J. Nick Koston 4f11db4232 Reject framework 3.1.0 (no registry package) and warn when platform_version is ignored 2026-08-20 05:16:51 -05:00
J. Nick Koston 311f610787 Make the link-line and tool-path tests platform-independent 2026-08-20 05:10:58 -05:00
J. Nick Koston 7f2515edf1 Share the PlatformIO flag-lexing helper and flatten the build_flags classifier 2026-08-20 05:03:49 -05:00
J. Nick Koston eb3a64829f Shell-lex -L/-l build_flags entries and generate the compile database before the build 2026-08-20 04:59:27 -05:00
J. Nick Koston 7cb22725e3 Escape object paths once at the source and drop a dead parameter 2026-08-20 04:39:50 -05:00
J. Nick Koston db010d486e Revert ninja command quoting (ninja escapes its own path variables); share the lib_ignore helper 2026-08-20 04:31:01 -05:00
J. Nick Koston c579a2792f Address review: quote ninja command paths, refuse unverified downloads, extend lib_ignore and build_unflags parity 2026-08-20 04:23:14 -05:00
J. Nick Koston 005c01e1e2 Address review: Windows binutils suffix, honor build_unflags against framework flags 2026-08-20 03:51:08 -05:00
J. Nick Koston 59c309083d Address review: include surgery constants in the ld stamp, require size sections, warn on missing extraScript 2026-08-20 03:36:44 -05:00
J. Nick Koston 9cf7ff94ac Resolve bare registry libraries at latest version like PlatformIO; tighten review cleanups 2026-08-20 03:07:07 -05:00
J. Nick Koston 702ef56a88 Address review: route project -L/-l to the link, fail on unresolvable libraries, remove dead ninja constants, tighten diagnostics 2026-08-20 02:54:00 -05:00
J. Nick Koston 7065beaabc Cover the native toolchain dispatch branches in idedata and analyze-memory 2026-08-20 01:53:50 -05:00
J. Nick Koston e331813299 Get ninja from the PyPI wheel instead of hardcoded release URLs and checksums 2026-08-20 01:52:07 -05:00
J. Nick Koston 0c48f749c2 Fix testing-mode segment requirements per linker script; simplify downloads and native command dispatch 2026-08-20 01:45:39 -05:00
J. Nick Koston 3ae229f218 Fix analyze-memory test path expectations on Windows 2026-08-20 01:38:21 -05:00
J. Nick Koston 5df08d36be Address review: wire analyze-memory and idedata for the native toolchain, warn on ignored platformio_options, verify downloads, surface silent failures 2026-08-20 01:34:15 -05:00
J. Nick Koston 60b7b874f1 Address review: route versioned bare libraries to the registry, deterministic MMU order, scoped toolchain branches, diagnostics 2026-08-20 00:00:51 -05:00
J. Nick Koston 66f6f5c004 Address review: cover all source suffixes, pass PIOPLATFORM through, link library -Wl flags 2026-08-19 23:50:16 -05:00
J. Nick Koston 9429243d2b Fix determine-jobs narrowing helper and add esp8266-native tests 2026-08-19 23:19:52 -05:00
J. Nick Koston 3fe9cac1fc Apply review cleanups to the native ESP8266 toolchain 2026-08-19 23:13:30 -05:00
J. Nick Koston 8811224c0e Add full test coverage for the native ESP8266 toolchain 2026-08-19 22:59:05 -05:00
J. Nick Koston 373bee8aca [esp8266] Add native toolchain that builds the Arduino core without PlatformIO 2026-08-19 22:31:46 -05:00
31 changed files with 4964 additions and 163 deletions
@@ -0,0 +1,39 @@
name: Cache Arduino ESP8266
description: >
Resolve the pinned Arduino core and xtensa toolchain versions and cache the
native ESP8266 install (~110 MB framework + toolchain; no ccache store, the
seed job saves before any compile runs). Exports
ESPHOME_ARDUINO8266_PREFIX to the job so every later step installs into
the cached path; the Python venv must already be restored. Mirrors
cache-esp-idf: only dev-branch pushes write the shared cache, everything
else restores.
runs:
using: composite
steps:
- name: Resolve the native toolchain cache key
# Versions are pinned in code, not a hashable file; resolve them so a
# bump changes the cache key. Assignment form so errexit catches a
# resolver failure.
id: version
shell: bash
run: |
# One owner for the install prefix: exported here and referenced by
# the cache steps below via env, so the caller's install and the
# cached path cannot diverge.
echo "ESPHOME_ARDUINO8266_PREFIX=$HOME/.esphome-arduino8266" >> "$GITHUB_ENV"
. venv/bin/activate
key=$(python -c 'from esphome.components.esp8266 import RECOMMENDED_ARDUINO_FRAMEWORK_VERSION as f; from esphome.arduino8266.framework import TOOLCHAIN_VERSION as t; print(f"{f}-{t}")')
[ -n "$key" ] || exit 1
echo "key=$key" >> "$GITHUB_OUTPUT"
- name: Cache the native toolchain (write on dev)
if: github.ref == 'refs/heads/dev'
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ env.ESPHOME_ARDUINO8266_PREFIX }}
key: ${{ runner.os }}-esp8266-native-${{ steps.version.outputs.key }}
- name: Restore the native toolchain (off dev)
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ env.ESPHOME_ARDUINO8266_PREFIX }}
key: ${{ runner.os }}-esp8266-native-${{ steps.version.outputs.key }}
+67 -1
View File
@@ -102,6 +102,8 @@ jobs:
device-builder: ${{ steps.determine.outputs.device-builder }}
esp32-platformio: ${{ steps.determine.outputs.esp32-platformio }}
esp32-platformio-components: ${{ steps.determine.outputs.esp32-platformio-components }}
esp8266-native: ${{ steps.determine.outputs.esp8266-native }}
esp8266-native-components: ${{ steps.determine.outputs.esp8266-native-components }}
changed-components: ${{ steps.determine.outputs.changed-components }}
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
@@ -165,6 +167,8 @@ jobs:
echo "device-builder=$(echo "$output" | jq -r '.device_builder')" >> $GITHUB_OUTPUT
echo "esp32-platformio=$(echo "$output" | jq -r '.esp32_platformio')" >> $GITHUB_OUTPUT
echo "esp32-platformio-components=$(echo "$output" | jq -r '.esp32_platformio_components')" >> $GITHUB_OUTPUT
echo "esp8266-native=$(echo "$output" | jq -r '.esp8266_native')" >> $GITHUB_OUTPUT
echo "esp8266-native-components=$(echo "$output" | jq -r '.esp8266_native_components')" >> $GITHUB_OUTPUT
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT
@@ -183,6 +187,32 @@ jobs:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
seed-esp8266-native-cache:
name: Seed the esp8266 native toolchain cache
runs-on: ubuntu-24.04
needs:
- common
# PR-branch cache saves are invisible to other PRs, so dev pushes seed
# the shared entry test-esp8266-native restores. Only dev: the composite
# action saves nowhere else, so a beta/release push would download the
# toolchain and discard it.
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
timeout-minutes: 15
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache the native toolchain
uses: ./.github/actions/cache-arduino8266
- name: Install the native toolchain
run: |
. venv/bin/activate
python -c "from esphome.arduino8266.framework import check_and_install; from esphome.components.esp8266 import RECOMMENDED_ARDUINO_FRAMEWORK_VERSION; check_and_install(RECOMMENDED_ARDUINO_FRAMEWORK_VERSION)"
ci-custom:
name: Run script/ci-custom
runs-on: ubuntu-24.04
@@ -1228,7 +1258,7 @@ jobs:
# compile validates config first, so a separate config pass is
# redundant for this smoke test. ESP-IDF framework via PlatformIO:
python3 script/test_build_components.py -e compile -t esp32-idf -c "$TEST_COMPONENTS" -f --toolchain platformio
python3 script/test_build_components.py -e compile -t esp32-idf -c "$TEST_COMPONENTS" -f --toolchain platformio --fail-on-no-tests
echo ""
echo "ESP-IDF-via-PlatformIO build passed! Starting Arduino smoke test..."
@@ -1237,6 +1267,40 @@ jobs:
# Arduino framework via PlatformIO (only components with an esp32-ard test are built):
python3 script/test_build_components.py -e compile -t esp32-ard -c "$TEST_COMPONENTS" -f --toolchain platformio
test-esp8266-native:
name: Test esp8266 components with the native toolchain
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: github.event_name == 'pull_request' && needs.determine-jobs.outputs.esp8266-native == 'true'
env:
# Computed by script/determine-jobs.py (ESP8266_NATIVE_TEST_COMPONENTS)
TEST_COMPONENTS: ${{ needs.determine-jobs.outputs.esp8266-native-components }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache the native toolchain
uses: ./.github/actions/cache-arduino8266
- name: Run native toolchain compile test
run: |
. venv/bin/activate
echo "Testing components: $TEST_COMPONENTS"
echo ""
# ESP8266 Arduino built directly (no PlatformIO); compile validates
# config first, so a separate config pass is redundant.
python3 script/test_build_components.py -e compile -t esp8266-ard -c "$TEST_COMPONENTS" -f --toolchain arduino --fail-on-no-tests
device-builder:
name: Test downstream esphome/device-builder
runs-on: ubuntu-24.04
@@ -1599,6 +1663,7 @@ jobs:
needs:
- common
- seed-apt-cache
- seed-esp8266-native-cache
- determine-jobs
- ci-custom
- pylint
@@ -1614,6 +1679,7 @@ jobs:
- clang-tidy-esp32-variants
- test-build-components-split
- test-esp32-platformio
- test-esp8266-native
- device-builder
- memory-impact-target-branch
- memory-impact-pr-branch
+83 -31
View File
@@ -817,7 +817,9 @@ def write_cpp_file() -> int:
from esphome.build_gen import espidf
espidf.write_project()
else:
elif not CORE.using_native_toolchain:
# Other native builds generate their project at compile time;
# never write a platformio.ini for them
from esphome.build_gen import platformio
platformio.write_project()
@@ -859,20 +861,14 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
toolchain.create_factory_bin()
toolchain.create_ota_bin()
toolchain.create_elf_copy()
from esphome.build_helpers.idedata import IDEDATA_BEST_EFFORT_ERRORS
from esphome.build_helpers.idedata import warn_if_idedata_missing
try:
if toolchain.get_idedata() is None:
_LOGGER.warning("No idedata was generated for this build")
except IDEDATA_BEST_EFFORT_ERRORS as err:
# The firmware already built; an idedata failure must not fail
# a successful build.
_LOGGER.warning(
"Could not generate idedata: %s (IDE, clang-tidy, and "
"memory-analysis data will be unavailable for this build)",
err,
)
_LOGGER.debug("Idedata failure detail", exc_info=True)
warn_if_idedata_missing(toolchain.get_idedata)
elif CORE.using_native_toolchain:
raise EsphomeError(
f"Toolchain '{CORE.toolchain.value}' resolved but no platform "
"backend claimed the build"
)
else:
from esphome.platformio import toolchain
@@ -975,12 +971,15 @@ def upload_using_esptool(
if file is not None:
flash_images = [FlashImage(path=file, offset="0x0")]
elif CORE.using_toolchain_esp_idf:
from esphome.espidf import toolchain
flash_images = [
FlashImage(path=toolchain.get_factory_firmware_path(), offset="0x0")
]
elif (native := _native_toolchain_module()) is not None:
# Every native backend supplies its own 0x0 flash image (bootloader
# and partitions included where the target needs them)
image = native.get_factory_firmware_path()
if not image.is_file():
raise EsphomeError(
f"{image} does not exist; compile the configuration first"
)
flash_images = [FlashImage(path=image, offset="0x0")]
else:
from esphome.platformio import toolchain
@@ -1950,15 +1949,39 @@ def command_update_all(args: ArgsProtocol) -> int | None:
return run_multiple_configs(files, build_command)
# Native build backend per (target platform, toolchain). Keyed here rather
# than through a platform hook so the serial upload/logs fast path never
# imports the platform component package (see the esp32 variant comment in
# upload_using_esptool); the platform half comes from CORE.data the same way.
_NATIVE_TOOLCHAIN_MODULES = {
("esp32", Toolchain.ESP_IDF): "esphome.espidf.toolchain",
("esp8266", Toolchain.ARDUINO): "esphome.arduino8266.toolchain",
}
def _native_toolchain_module():
"""The native build backend module for the resolved toolchain."""
if not CORE.using_native_toolchain:
return None
key = (CORE.target_platform, CORE.toolchain)
if (module_path := _NATIVE_TOOLCHAIN_MODULES.get(key)) is None:
# Degrading to the PlatformIO path would build with the wrong backend
raise EsphomeError(
f"Toolchain '{CORE.toolchain.value}' has no native build backend "
f"module for platform {CORE.target_platform}"
)
return importlib.import_module(module_path)
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
import json
if CORE.using_toolchain_esp_idf:
# Native ESP-IDF derives idedata from the build's compile_commands.json,
# so the configuration must already be compiled.
from esphome.espidf import toolchain as espidf_toolchain
native_toolchain = _native_toolchain_module()
idedata = espidf_toolchain.get_idedata()
if native_toolchain is not None:
# Native toolchains derive idedata from the build's
# compile_commands.json, so the configuration must already be compiled.
idedata = native_toolchain.get_idedata()
if idedata is None:
_LOGGER.error(
"No idedata available; compile the configuration first",
@@ -1997,6 +2020,17 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
from esphome.analyze_memory.cli import MemoryAnalyzerCLI
from esphome.analyze_memory.ram_strings import RamStringsAnalyzer
# Refuse an unsupported toolchain before paying for a full compile
native_toolchain = _native_toolchain_module()
if native_toolchain is None and not CORE.using_toolchain_platformio:
_LOGGER.error(
"analyze-memory is not supported with the '%s' toolchain on %s; "
"re-run with --toolchain platformio",
CORE.toolchain.value if CORE.toolchain else "unresolved",
CORE.target_platform,
)
return 1
# Always compile to ensure fresh data (fast if no changes - just relinks)
exit_code = write_cpp(config)
if exit_code != 0:
@@ -2008,13 +2042,31 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
# Get idedata for analysis
idedata = None
if CORE.using_toolchain_esp_idf:
from esphome.espidf import toolchain
if native_toolchain is not None:
objdump = native_toolchain.get_objdump_path()
readelf = native_toolchain.get_readelf_path()
for tool in (objdump, readelf):
if not tool.is_file():
# The analyzer would silently fall back to host binutils,
# which cannot read the target ELF. clean-all is heavy for
# ESP-IDF, so suggest a recompile first.
_LOGGER.error(
"%s is missing; the toolchain install may be incomplete "
"(recompile, or run 'esphome clean-all' if it persists)",
tool,
)
return 1
objdump_path = str(objdump)
readelf_path = str(readelf)
objdump_path = str(toolchain.get_objdump_path())
readelf_path = str(toolchain.get_readelf_path())
firmware_elf = toolchain.get_elf_path()
firmware_elf = native_toolchain.get_elf_path()
if not firmware_elf.is_file():
# The analyzer swallows tool failures, so a missing ELF would
# produce an exit-0 zeroed report
_LOGGER.error(
"%s is missing; compile the configuration first", firmware_elf
)
return 1
else:
from esphome.platformio import toolchain
+4 -1
View File
@@ -19,6 +19,7 @@ from typing import NamedTuple
from esphome.build_helpers.ccache import ccache_defaults_env
from esphome.build_helpers.ninja import find_ninja
from esphome.build_helpers.pch import ccache_pch_env
from esphome.build_helpers.tools_cache import ARDUINO8266_TOOLS_CACHE, tools_cache_path
from esphome.core import EsphomeError, Version
from esphome.framework_helpers import str_to_lst_of_str
@@ -156,4 +157,6 @@ def ccache_env(ccache: str | None) -> dict[str, str]:
"""
if ccache is None:
return {}
return ccache_defaults_env(get_arduino8266_tools_path() / "ccache")
env = ccache_defaults_env(get_arduino8266_tools_path() / "ccache")
env.update(ccache_pch_env())
return env
+329
View File
@@ -0,0 +1,329 @@
"""Native Arduino ESP8266 build driver (the PlatformIO ``run`` equivalent)."""
from __future__ import annotations
import json
import logging
from pathlib import Path
import subprocess
from typing import Any
from esphome.arduino8266 import framework
from esphome.build_helpers.ccache import resolve_ccache_path
from esphome.const import (
CONF_COMPILE_PROCESS_LIMIT,
CONF_ESPHOME,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
)
from esphome.core import CORE, EsphomeError
from esphome.helpers import write_file_if_changed
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
# ESP8266 user RAM (matches upload.maximum_ram_size in every board manifest)
_MAX_RAM_SIZE = 81920
def _warn_ignored_platformio_options() -> None:
"""Warn for component-added platformio options the native build drops.
The consumed set is exported by core/config.py next to the routing that
stores these options, so the two cannot drift; YAML upload_speed never
reaches CORE.platformio_options here.
"""
from esphome.core.config import NATIVE_ARDUINO_CONSUMED_PIO_OPTIONS
consumed = NATIVE_ARDUINO_CONSUMED_PIO_OPTIONS
for key in sorted(CORE.platformio_options or {}):
if key not in consumed:
_LOGGER.warning(
"platformio_options->%s is ignored when building with the "
"native 'arduino' toolchain",
key,
)
_RAM_SECTIONS = (".data", ".rodata", ".bss")
_FLASH_SECTIONS = (".irom0.text", ".text", ".text1", ".data", ".rodata")
def get_build_dir() -> Path:
return CORE.relative_pioenvs_path(CORE.name)
def get_elf_path() -> Path:
return get_build_dir() / "firmware.elf"
def _toolchain_tool(name: str) -> Path:
return framework.toolchain_tool(framework.get_toolchain_path(), name)
def get_factory_firmware_path() -> Path:
"""The image to serial-flash at 0x0 (same bytes as firmware.bin: the
8266 factory copy exists for artifact-contract parity, not content)."""
return get_build_dir() / "firmware.factory.bin"
def get_addr2line_path() -> Path:
return _toolchain_tool("addr2line")
def get_objdump_path() -> Path:
return _toolchain_tool("objdump")
def get_readelf_path() -> Path:
return _toolchain_tool("readelf")
def run_compile(config: ConfigType, verbose: bool) -> int:
from esphome.build_gen import arduino8266 as build_gen
_warn_ignored_platformio_options()
paths = framework.check_and_install(CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION])
# Resolved once per build: the resolution probes PATH and spawns the
# runnability check, and three consumers need the same answer
ccache = resolve_ccache_path()
ninja_changed = build_gen.write_project(paths, ccache)
build_dir = get_build_dir()
env = framework.get_build_env(paths.toolchain, ccache)
# Regenerate the compile DB before the build (a pure function of
# build.ninja); skip only when it is at least as fresh as build.ninja
# (an interrupted previous run may have rewritten the manifest without
# regenerating the DB).
compdb = build_dir / "compile_commands.json"
compdb_stamp = build_dir / ".compile_commands.stamp"
ninja_file = build_dir / "build.ninja"
# Freshness rides a stamp: the DB itself is written through
# write_file_if_changed (its mtime feeds get_idedata's cache), so a
# regeneration with identical content would stay "stale" forever
if (
ninja_changed
or not compdb.is_file()
or not compdb_stamp.is_file()
or compdb_stamp.stat().st_mtime < ninja_file.stat().st_mtime
):
_write_compile_commands(paths.ninja, build_dir, env)
compdb_stamp.touch()
cmd = [str(paths.ninja)]
if verbose:
cmd.append("-v")
if jobs := config[CONF_ESPHOME].get(CONF_COMPILE_PROCESS_LIMIT):
cmd += ["-j", str(jobs)]
# Explicit targets, not the default statement: a generator defect that
# drops them fails loudly with "unknown target" instead of a green
# no-op run that leaves stale artifacts in place
targets = ["firmware.factory.bin", "firmware.ota.bin"]
cmd += targets
# A dry-run probe keeps a no-op rebuild quiet: ninja would only print
# "no work to do". A freshly rewritten manifest all but guarantees work,
# so skip the probe (and its full stat pass) on that path. cwd instead
# of -C also drops the "Entering directory" banner on real builds.
skip_build = False
if not ninja_changed:
probe = subprocess.run(
[str(paths.ninja), "-n", *targets],
cwd=build_dir,
env=env,
capture_output=True,
text=True,
check=False,
close_fds=False,
)
if probe.stderr.strip():
# A load-time diagnostic (e.g. "multiple rules generate X")
# flags a generator bug; the skip branch would otherwise
# swallow it forever
_LOGGER.warning("ninja: %s", probe.stderr.strip())
if probe.returncode != 0:
# An unknown target here is the defective-manifest case; fall
# through to the real build so the error prints attributably
_LOGGER.debug("ninja probe failed; running the full build")
skip_build = probe.returncode == 0 and "no work to do" in probe.stdout
if skip_build:
_LOGGER.debug("ninja: nothing to rebuild")
else:
_LOGGER.debug("Running: %s", " ".join(cmd))
rc = subprocess.run(
cmd, cwd=build_dir, env=env, check=False, close_fds=False
).returncode
if rc != 0:
return rc
# ninja already refused a manifest missing the explicit targets above;
# existence covers the remaining hole (a rule that ran but wrote
# elsewhere). The factory/ota copies are what upload and OTA consume.
build_dir_artifacts = (
get_elf_path(),
build_dir / "firmware.bin",
get_factory_firmware_path(),
build_dir / "firmware.ota.bin",
)
for artifact in build_dir_artifacts:
if not artifact.is_file():
_LOGGER.error("Build produced no %s", artifact)
return 1
if not _print_size_summary(build_dir, paths):
# The cause was already warned; name the consequence so a build
# contributing no RAM/Flash metric is visible to CI harnesses
_LOGGER.warning("Firmware size summary unavailable for this build")
from esphome.build_helpers.idedata import warn_if_idedata_missing
warn_if_idedata_missing(lambda: get_idedata(ccache))
return 0
def _write_compile_commands(
ninja_path: Path, build_dir: Path, env: dict[str, str]
) -> None:
compdb = build_dir / "compile_commands.json"
result = subprocess.run(
[str(ninja_path), "-C", str(build_dir), "-t", "compdb", "c", "cxx", "asm"],
env=env,
capture_output=True,
text=True,
check=False,
close_fds=False,
)
if result.returncode != 0:
# Drop any stale database so consumers (IDE integration, clang-tidy,
# the memory analyzer) can't silently read outdated data.
compdb.unlink(missing_ok=True)
raise EsphomeError(f"Could not generate compile_commands.json: {result.stderr}")
try:
entries = json.loads(result.stdout)
except ValueError as err:
compdb.unlink(missing_ok=True)
raise EsphomeError(
f"ninja produced an unparsable compile database: {err} "
f"(output starts {result.stdout[:120]!r})"
) from err
if not entries:
# compdb exits 0 with [] for unknown rule names; a renamed compile
# rule must fail the build, not silently strand every consumer
compdb.unlink(missing_ok=True)
raise EsphomeError(
"ninja produced an empty compile database; the generator's rule "
"names no longer match"
)
# write_file_if_changed keeps the mtime stable on no-op builds so the
# idedata cache in get_idedata() stays valid.
write_file_if_changed(compdb, result.stdout)
def _parse_app_size(build_dir: Path, paths: framework.InstalledPaths) -> int | None:
"""Read the app flash budget (irom0_0_seg length) from the linker script."""
from esphome.build_gen.arduino8266 import get_flash_ld_path
from esphome.components.esp8266.build_surgery import segment_length
# Warnings, not debug: without the app size the Flash summary line is
# dropped and CI's memory-impact extraction loses its flash metric.
ld_path = get_flash_ld_path(build_dir, paths)
try:
ld_text = ld_path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as err:
# UnicodeDecodeError: a truncated/corrupt script must degrade to
# the same warning, never abort an already-linked build
_LOGGER.warning("Cannot read linker script for the Flash summary: %s", err)
return None
app_size = segment_length(ld_text, "irom0_0_seg")
if app_size is None:
_LOGGER.warning("irom0_0_seg not found in %s; skipping Flash summary", ld_path)
return None
if app_size == 0:
_LOGGER.warning(
"irom0_0_seg has zero length in %s; skipping Flash summary", ld_path
)
return None
return app_size
def _print_size_summary(build_dir: Path, paths: framework.InstalledPaths) -> bool:
"""Print the PlatformIO-shaped RAM/Flash lines; False when skipped.
The exact shape (including the bar) is parsed by
``script/ci_memory_impact_extract.py``; ``print_size_line`` matches it.
"""
from esphome.build_helpers.size_summary import print_size_line
size_tool = _toolchain_tool("size")
try:
result = subprocess.run(
[str(size_tool), "-A", "-d", str(get_elf_path())],
capture_output=True,
text=True,
check=False,
close_fds=False,
)
except OSError as err:
# The summary is a bonus artifact like idedata; a truncated
# toolchain extraction must not discard an already-linked build
_LOGGER.warning("Could not summarize firmware size: %s", err)
return False
if result.returncode != 0:
_LOGGER.warning("Could not summarize firmware size: %s", result.stderr)
return False
sections: dict[str, int] = {}
for line in result.stdout.splitlines():
parts = line.split()
if len(parts) >= 2 and parts[0].startswith("."):
try:
sections[parts[0]] = int(parts[1])
except ValueError:
# An unparsed RAM/Flash section trips the missing-sections
# guard below, so no total is built on a dropped value
_LOGGER.warning("Unparsable size output for section %s", parts[0])
if missing := set(_RAM_SECTIONS + _FLASH_SECTIONS) - set(sections):
# A defaulted 0 would print a confidently wrong total for CI's metric
_LOGGER.warning(
"Size output is missing section(s) %s; skipping the size summary",
", ".join(sorted(missing)),
)
return False
# Resolve the flash budget before printing anything: a RAM line without
# its Flash line would let CI's memory-impact extraction sum the two
# metrics over different build counts (_parse_app_size already warned).
app_size = _parse_app_size(build_dir, paths)
if not app_size:
return False
ram = sum(sections[s] for s in _RAM_SECTIONS)
flash = sum(sections[s] for s in _FLASH_SECTIONS)
print_size_line("RAM", ram, _MAX_RAM_SIZE)
print_size_line("Flash", flash, app_size)
return True
# Sentinel: "resolve for me"; None is a real value meaning disabled.
_CCACHE_UNRESOLVED: Any = object()
def get_idedata(ccache: str | None = _CCACHE_UNRESOLVED) -> dict | None:
"""Derive idedata from the build's compile_commands.json.
Same contract as ``espidf.toolchain.get_idedata``: the fields IDE
integrations, clang-tidy, and the memory analyzer expect.
"""
from esphome.build_helpers.idedata import load_or_build_idedata
if ccache is _CCACHE_UNRESOLVED:
# Deliberately uncached: env/PATH can change between builds in a
# long-lived host process
ccache = resolve_ccache_path()
return load_or_build_idedata(
get_build_dir() / "compile_commands.json",
get_elf_path(),
# Suffixed so a platformio->arduino->platformio round trip on one
# config never serves the other toolchain's cache shape
CORE.relative_internal_path("idedata", f"{CORE.name}.arduino.json"),
# The compile DB's commands carry the same ccache prefix the ninja
# rules were generated with
launcher=str(ccache) if ccache else None,
)
+570 -7
View File
@@ -1,11 +1,11 @@
"""Build specification for the native ESP8266 Arduino toolchain.
"""Native ninja build generator for the ESP8266 Arduino core.
Transliterates the PlatformIO build spec for the Arduino ESP8266 framework
(``framework-arduinoespressif8266/tools/platformio-build.py`` plus
``platform-espressif8266/builder/main.py``): the flag sets, defines, and
linker-script generation deliberately match what PlatformIO produces so the
binaries stay near-identical between the two toolchains. The ninja emission
(``write_project``) builds on these pieces.
``platform-espressif8266/builder/main.py``) into a ``build.ninja`` under
``.pioenvs/<name>/``. The flag sets, defines, link line, linker-script
generation, and ``elf2bin`` invocation deliberately match what PlatformIO
produces so the binaries stay near-identical between the two toolchains.
The ``PIO_FRAMEWORK_ARDUINO_*`` knob defines (lwIP variant, NONOS SDK
version, MMU layout, exceptions, waveform phase) keep working: they are read
@@ -14,6 +14,7 @@ from the build flags with the same precedence as the PlatformIO builder.
from __future__ import annotations
from collections.abc import Collection
from dataclasses import dataclass
import hashlib
import logging
@@ -22,20 +23,61 @@ from pathlib import Path
import re
import shlex
import subprocess
import sys
from typing import TYPE_CHECKING, NamedTuple
from esphome.arduino8266.framework import toolchain_tool
from esphome.build_helpers.ninja import shell_token as _shell_token
from esphome.build_helpers.ccache import effective_ccache_basedir
from esphome.build_helpers.idedata import is_joined_include
from esphome.build_helpers.ninja import (
escape as _e,
quote_path as _q,
shell_token as _shell_token,
)
from esphome.build_helpers.pch import (
PCH_CORE_HEADER,
PCH_HEADER_NAME,
mark_pch_emitted,
pch_checksum,
pch_enabled,
pch_header_text,
)
from esphome.components.esp8266 import build_surgery
from esphome.components.esp8266.boards import (
BOARDS,
ESP8266_BOARD_BUILD,
board_ld_script,
)
from esphome.components.esp8266.const import (
BUILD_FLASH_MODES,
KEY_BOARD,
KEY_ESP8266,
KEY_FLASH_SIZE,
KEY_SCANF_FLOAT,
)
from esphome.core import CORE, EsphomeError
from esphome.framework_helpers import (
get_project_cxx_compile_flags,
strip_win_long_path_prefix,
)
from esphome.helpers import mkdir_p, write_file_if_changed
from esphome.platformio.library import lex_build_flags
from esphome.platformio.library import SOURCE_KIND_FOR_SUFFIX, lex_build_flags
if TYPE_CHECKING:
from esphome.arduino8266.framework import InstalledPaths
_LOGGER = logging.getLogger(__name__)
# Always excluded from the core build: ESPHome uses its own native OTA
# backend, so the Arduino Updater (and its 228-byte global) never links.
_CORE_EXCLUDE_ALWAYS = {"Updater.cpp"}
# Excluded when no component called require_waveform(); waveform_stubs.cpp
# supplies the stopWaveform()/_stopPWM() stubs digitalWrite needs.
_CORE_EXCLUDE_WAVEFORM = {
"core_esp8266_waveform_pwm.cpp",
"core_esp8266_waveform_phase.cpp",
}
# Values that land unquoted on generated command lines are shape-checked
# against these before use. re.ASCII: a Unicode digit or word character
# (Arabic-Indic numerals) would pass \d/\w and defeat the named error
@@ -77,6 +119,11 @@ def _apply_surgery(fn, *args: object) -> str:
raise EsphomeError(str(err)) from err
# Every supported board's f_flash is 40 MHz; re-check on a platform bump
# board_flash_mode's closed set, shared with cv.one_of's validation
_FLASH_MODES = frozenset(BUILD_FLASH_MODES)
_FLASH_FREQ_MHZ = 40
# From platformio-build.py. Knob suffix -> SDK define; the first entry is
# the default (dicts preserve insertion order). With multiple SDK knobs set
# (a pathological config) ties break by table order, since upstream's
@@ -408,6 +455,52 @@ def _resolve_build_config(defines: dict[str, str]) -> _BuildConfig:
)
_INCOMPLETE_INSTALL = "Arduino toolchain install is incomplete"
def _filter_link_flags(unflags: set[str]) -> list[str]:
"""_LINKFLAGS minus ``unflags``, pair-aware: unflagging a symbol also
drops the ``-u`` that carried it, so no dangling operand-less flag
reaches ld as the next token's consumer."""
out: list[str] = []
it = iter(_LINKFLAGS)
for tok in it:
if tok == "-u":
symbol = next(it)
if symbol not in unflags:
out += [tok, symbol]
elif tok not in unflags:
out.append(tok)
return out
def _active_flash_ld_name(flash_ld_name: str) -> str:
"""The flash linker-script filename the link uses (testing mode renames
the surgically patched copy)."""
return (
f"{_TESTING_LD_PREFIX}{flash_ld_name}" if CORE.testing_mode else flash_ld_name
)
def _flash_ld_name(board: str) -> str:
"""The flash linker script: the board's, or a routed user override.
Published configs override board_build.ldscript to reserve a
filesystem region or correct a board's assumed flash size; a bare
name is required because the script resolves via the -L search path.
"""
override = _pio_option("board_build.ldscript", "")
if not override:
# The same shared rule the PlatformIO path pins (layout
# preservation, see boards.board_ld_script)
return board_ld_script(BOARDS[board])
if Path(override).name != override:
raise EsphomeError(
f"board_build.ldscript must be a bare script name, got {override!r}"
)
return override
def _pio_option(key: str, default: str) -> str:
"""A platformio_options value the native build honors (str-normalized).
@@ -551,11 +644,23 @@ _PLAIN_LINKER_FLAGS = (
"-nostdlib",
"-rdynamic",
)
# The subset whose next token is an operand; unflagging the bare flag
# would strand the operand. Operand-less members of the list above filter
# whole-token from both the compile and link lines, as PlatformIO allows.
_PLAIN_LINKER_OPERAND_FLAGS = ("-u", "-e")
_PLAIN_LINKER_PREFIXES = ("-T", "-Xlinker")
# Driver options, not ld options: -Wl, has no equivalent for these
_PLAIN_DRIVER_LINK_PREFIXES = ("-fuse-ld=", "--specs=", "-specs=")
def _collect_sources(root: Path, exclude: Collection[str] = frozenset()) -> list[Path]:
return sorted(
p
for p in root.rglob("*")
if p.suffix in SOURCE_KIND_FOR_SUFFIX and p.name not in exclude
)
def _stat_sig(path: Path) -> str:
"""Size and mtime cache-stamp signature for one input file.
@@ -771,3 +876,461 @@ def _generate_testing_flash_ld(
write_file_if_changed(
ld_dir / f"{_TESTING_LD_PREFIX}{flash_ld_name}", patched_flash_ld
)
def _ninja_compile_edges(
lines: list[str],
sources: list[Path],
root: Path,
group: str,
flags: str = "",
cxx_override: tuple[str, str] | None = None,
) -> list[str]:
"""Emit compile edges for ``sources``; return the object paths.
``cxx_override`` is a (flags, implicit-dep) pair applied to C++ edges
only, replacing ``flags`` (used for the precompiled header).
"""
objects = []
for src in sources:
rel = src.relative_to(root).as_posix()
obj = f"obj/{group}/{rel}.o"
escaped_obj = _e(obj)
kind = SOURCE_KIND_FOR_SUFFIX[src.suffix]
override = cxx_override if kind == "cxx" and cxx_override else None
implicit = f" | {override[1]}" if override else ""
lines.append(f"build {escaped_obj}: {kind} {_e(src)}{implicit}")
edge_flags = override[0] if override else flags
if edge_flags:
lines.append(f" flags = {edge_flags}")
# Escaped once here: the returned paths only ever appear in build
# statements (archive/link inputs), which use ninja escaping.
objects.append(escaped_obj)
return objects
def _common_parent(paths: list[Path]) -> Path:
return Path(os.path.commonpath([str(p.parent) for p in paths]))
def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
"""Write the ninja build for the current configuration.
``ccache`` is the caller's already-resolved binary (None when disabled)
so one build never pays the runnability probe per consumer. Returns
True when ``build.ninja`` changed, so the caller can skip work derived
purely from it (the compile database) on unchanged builds.
"""
from esphome.arduino.library import resolve_libraries
framework = paths.framework
toolchain_bin = paths.toolchain / "bin"
build_dir = CORE.relative_pioenvs_path(CORE.name)
mkdir_p(build_dir)
unflags = _unflag_tokens()
# Lexed once so a malformed entry warns once, not per consumer
build_tokens = _lexed_build_flags()
flag_defines = _flag_defines(unflags, build_tokens)
config = _resolve_build_config(flag_defines)
esp8266_data = CORE.data[KEY_ESP8266]
board = esp8266_data[KEY_BOARD]
# Config validation already gates boards;
# kept as defense-in-depth for direct calls, since CONF_BOARD itself is
# a free-form string
if board not in ESP8266_BOARD_BUILD:
raise EsphomeError(f"Board '{board}' is not supported by the native toolchain")
board_build = ESP8266_BOARD_BUILD[board]
# From the same producer the PlatformIO path reads (one source)
flash_mode = _pio_option("board_build.flash_mode", "dout")
if flash_mode not in _FLASH_MODES:
# Lands unquoted in the elf2bin command and a -D body; validation
# (cv.one_of on board_flash_mode) already gates it, defense-in-depth
raise EsphomeError(f"Invalid flash mode {flash_mode!r}")
flash_ld_name = _flash_ld_name(board)
generate_ld_scripts(paths, config, flash_ld_name)
sdk = framework / "tools" / "sdk"
core_dir = framework / "cores" / "esp8266"
variant_dir = framework / "variants" / board_build["variant"]
src_dir = CORE.relative_src_path()
libraries = resolve_libraries(
framework,
pio_platform="espressif8266",
board_mcu="esp8266",
cache_key="arduino8266",
)
if not src_dir.is_dir():
# Generated project state, not install state: clean-all would not help
raise EsphomeError(f"Generated source directory {src_dir} is missing")
# A missing install directory would otherwise surface as a wall of
# include errors; failing here names the path instead.
include_dirs = [
src_dir,
sdk / "include",
core_dir,
paths.toolchain / "include",
sdk / "lwip2" / "include",
variant_dir,
]
for required in include_dirs[1:]:
if not required.is_dir():
raise EsphomeError(
f"{_INCOMPLETE_INSTALL}: missing {required}; {_CLEAN_HINT}"
)
# The elf2bin edge runs after the full compile and link; a
# half-extracted package must fail here, not an hour of wall-clock later
for required_file in (
framework / "tools" / "elf2bin.py",
framework / "bootloaders" / "eboot" / "eboot.elf",
):
if not required_file.is_file():
raise EsphomeError(
f"{_INCOMPLETE_INSTALL}: missing {required_file}; {_CLEAN_HINT}"
)
for lib in libraries:
include_dirs += lib.include_dirs
(
project_compile_flags,
project_link_flags,
project_lib_dirs,
project_libs,
) = _project_flags(unflags, build_tokens)
defines = _defines_flags(config, flash_mode, board, board_build["defines"])
includes = [f"-I{_q(d)}" for d in include_dirs]
common = _CCFLAGS + defines + includes + project_compile_flags
cflags = _CFLAGS + common
cpp_standard = CORE.cpp_standard or "gnu++17"
cxxflags = (
["-fno-rtti", f"-std={cpp_standard}"]
+ ["-fexceptions" if config.exceptions else "-fno-exceptions"]
+ common
+ [_shell_token(f) for f in get_project_cxx_compile_flags()]
)
# PlatformIO's ASPPCOM passes only -D/-I user flags to assembly; match
# it (tokens arrive shell-quoted, hence the lstrip)
asflags = (
_ASFLAGS
+ defines
+ includes
+ [f for f in project_compile_flags if f.lstrip("\"'").startswith(("-D", "-I"))]
)
# build_unflags applies to the framework flag sets too (compile and link),
# as under PlatformIO (a silently ignored ``build_unflags: -Os`` would
# diverge between the toolchains).
# Matching is whole-token, so an unflag that hits nothing in the user
# flags or any framework set (a typo, or -DUSE_FOO against -DUSE_FOO=1)
# must be visible: the user believes the flag is gone while it still
# drives the compile line and the knob selection
flag_universe = set(build_tokens)
for flags in (cflags, cxxflags, asflags, _LINKFLAGS):
flag_universe.update(flags)
if unmatched := sorted(unflags - flag_universe):
_LOGGER.warning(
"build_unflags entries matched no build flag: %s", ", ".join(unmatched)
)
# _LINKFLAGS stores -u and its operand as two tokens; unflagging the
# bare -u would strip all seven and leave the operands as ld "input
# files" with an error pointing nowhere near build_unflags
if plain := sorted(
u
for u in unflags
if u in _PLAIN_LINKER_OPERAND_FLAGS or u.startswith(_PLAIN_LINKER_PREFIXES)
):
raise EsphomeError(
f"build_unflags cannot remove plain linker flag(s) "
f"{', '.join(plain)}; unflag the full -Wl, form or the symbol"
)
cflags, cxxflags, asflags = (
[f for f in flags if f not in unflags] for flags in (cflags, cxxflags, asflags)
)
link_flags = _filter_link_flags(unflags)
if esp8266_data[KEY_SCANF_FLOAT]:
link_flags += ["-u", "_scanf_float"]
link_flags += project_link_flags
link_flags += [_shell_token(flag) for lib in libraries for flag in lib.link_flags]
flash_ld = _active_flash_ld_name(flash_ld_name)
# A user-overridden script name re-quotes like every other user token
link_flags += ["-T", _shell_token(flash_ld)]
lib_dirs = [Path("ld"), sdk / "lib", sdk / "ld", sdk / "lib" / config.nonosdk]
lib_dirs += project_lib_dirs
for lib in libraries:
lib_dirs += lib.link_dirs
system_libs = (
_SYSTEM_LIBS_PRE_LWIP
+ [config.lwip_lib]
+ _SYSTEM_LIBS_POST_LWIP
+ project_libs
+ [lib_name for lib in libraries for lib_name in lib.link_libs]
+ ["stdc++-exc" if config.exceptions else "stdc++", "m", "c", "gcc"]
)
build_tool = Path(__file__).parent / "build_tool.py"
# $in/$out stay unquoted: ninja escapes its built-in path variables
# itself; only literal paths need _q().
lines = [
"# Auto-generated by ESPHome",
"ninja_required_version = 1.5",
f"cc = {_q(toolchain_tool(paths.toolchain, 'gcc'))}",
f"cxx = {_q(toolchain_tool(paths.toolchain, 'g++'))}",
# The NSIS launcher starts Python with a \\?\ extended-length path
# that cmd.exe cannot spawn; same strip every other emitted binary
# path gets
f"python = {_q(strip_win_long_path_prefix(sys.executable))}",
f"buildtool = {_q(build_tool)}",
f"ccache = {_q(ccache) if ccache else ''}",
"",
# Rule names match SOURCE_KIND_FOR_SUFFIX values (c, cxx, asm, aspp)
"rule c",
" command = $ccache $cc -MMD -MF $out.d $cflags $flags -c $in -o $out",
" depfile = $out.d",
" deps = gcc",
" description = CC $out",
"rule cxx",
" command = $ccache $cxx -MMD -MF $out.d $cxxflags $flags -c $in -o $out",
" depfile = $out.d",
" deps = gcc",
" description = CXX $out",
"rule aspp",
" command = $ccache $cc -MMD -MF $out.d -x assembler-with-cpp $asflags $flags -c $in -o $out",
" depfile = $out.d",
" deps = gcc",
" description = AS $out",
# No $ccache: the .gch is compiled once per build dir and ccache
# cannot cache it usefully (its bytes embed build-dir paths)
"rule pch",
" command = $cxx -MMD -MF $out.d -x c++-header $cxxflags $flags -c $in -o $out",
" depfile = $out.d",
" deps = gcc",
" description = PCH $out",
# Plain assembler, as SCons's ASCOM: no preprocessor, so no
# depfile and no $flags (defines/includes) either
"rule asm",
" command = $ccache $cc -x assembler $asflags -c $in -o $out",
" description = AS $out",
"rule ar",
f" command = $python $buildtool ar {_q(toolchain_tool(paths.toolchain, 'ar'))} $out $out.rsp",
" rspfile = $out.rsp",
" rspfile_content = $in_newline",
" description = AR $out",
"rule link",
" command = $cxx -o $out $linkflags @$out.rsp $libdirflags -Wl,--start-group $archives $libflags -Wl,--end-group",
" rspfile = $out.rsp",
" rspfile_content = $in_newline",
" description = LINK $out",
"rule elf2bin",
# --flash_size deliberately stays board-derived, as under
# PlatformIO (which reads upload.maximum_size, not the ldscript).
# -W: the framework's own elf2bin.py trips SyntaxWarning on 3.12+.
f" command = $python -W ignore::SyntaxWarning {_q(framework / 'tools' / 'elf2bin.py')} --eboot {_q(framework / 'bootloaders' / 'eboot' / 'eboot.elf')} --app $in --flash_mode {flash_mode} --flash_freq {_FLASH_FREQ_MHZ} --flash_size {_flash_size_str(BOARDS[board][KEY_FLASH_SIZE])} --path {_q(toolchain_bin)} --out $out",
" description = BIN $out",
"rule copy",
" command = $python $buildtool copy $in $out",
" description = COPY $out",
"",
f"cflags = {' '.join(cflags)}",
f"cxxflags = {' '.join(cxxflags)}",
f"asflags = {' '.join(asflags)}",
f"linkflags = {' '.join(link_flags)}",
f"libdirflags = {' '.join(f'-L{_q(d)}' for d in lib_dirs)}",
f"libflags = {' '.join(_shell_token(f'-l{lib}') for lib in system_libs)}",
"",
]
core_exclude = set(_CORE_EXCLUDE_ALWAYS)
if "USE_ESP8266_WAVEFORM_STUBS" in flag_defines:
core_exclude |= _CORE_EXCLUDE_WAVEFORM
archives = []
direct_objs: list[str] = []
# variant_dir existence was already enforced with the include dirs
variant_sources = _collect_sources(variant_dir)
if variant_sources:
objs = _ninja_compile_edges(lines, variant_sources, variant_dir, "variant")
lines.append(f"build libFrameworkArduinoVariant.a: ar {' '.join(objs)}")
archives.append("libFrameworkArduinoVariant.a")
core_objs = _ninja_compile_edges(
lines, _collect_sources(core_dir, core_exclude), core_dir, "core"
)
if not core_objs:
# An empty archive would link into a wall of undefined references
# (app_entry, the exception vectors) far from the cause
raise EsphomeError(
f"{_INCOMPLETE_INSTALL}: no core sources in {core_dir}; {_CLEAN_HINT}"
)
lines.append(f"build libFrameworkArduino.a: ar {' '.join(core_objs)}")
archives.append("libFrameworkArduino.a")
for lib in libraries:
if not lib.sources:
# Header-only libraries are legitimate; the log makes an empty
# srcFilter or broken tree traceable before link errors do.
_LOGGER.debug(
"Library %s has no source files; contributing includes only",
lib.name,
)
continue
lib_root = _common_parent(lib.sources)
objs = _ninja_compile_edges(
lines,
lib.sources,
lib_root,
f"lib/{lib.name}",
flags=" ".join(_shell_token(f) for f in lib.flags),
)
if not lib.lib_archive:
# libArchive: false / dot_a_linkage=false: hand the objects to
# the linker directly so unreferenced-but-required symbols
# (exception handlers, weak overrides) survive
direct_objs.extend(objs)
continue
archive = f"lib{lib.name}.a"
lines.append(f"build {_e(archive)}: ar {' '.join(objs)}")
archives.append(archive)
# One source of truth with the PlatformIO path: esp8266/__init__ pins
# build_src_flags (the throw_stubs force-include); -include paths
# resolve against the source root
src_other: list[str] = []
src_includes: list[str] = []
src_it = iter(
lex_build_flags(_pio_option("build_src_flags", ""), "build_src_flags")
)
for tok in src_it:
if tok == "-include":
header = next(src_it, "")
if not header:
raise EsphomeError(
"build_src_flags has a trailing '-include' with no header"
)
src_includes.append(header)
elif is_joined_include(tok):
# Left in src_other it would precede the pch include and
# silently defeat the .gch
src_includes.append(tok[len("-include") :])
else:
src_other.append(_shell_token(tok))
include_flags = [f"-include {_q(src_dir / h)}" for h in src_includes]
# One shared variable instead of repeating the flags line on every src
# edge (hundreds of edges in a real project)
lines.append(f"srcflags = {' '.join(src_other + include_flags)}")
src_cxx_override = None
if pch_enabled() and any(tok.startswith("-include") for tok in cxxflags):
# $cxxflags expands first, so a user -include there means GCC would
# never load the .gch
_LOGGER.warning(
"A -include in build_flags prevents the precompiled header from "
"loading; compiling without it"
)
elif pch_enabled():
# C++ src edges swap the force-includes for one precompiled prefix
# header (same content plus defines.h); C/assembly keep srcflags
pch_header = build_dir / PCH_HEADER_NAME
pch_includes = (*src_includes, PCH_CORE_HEADER)
pch_text = pch_header_text(pch_includes)
checksum = None
try:
if ccache:
# The .sum exists only for CCACHE_PCH_EXTSUM; ninja's depfile
# handles staleness. Strip resolved and raw build paths
# (symlinks) so identical configs share cache entries
flags_id = (
" ".join(cxxflags)
.replace(effective_ccache_basedir(), "")
.replace(str(CORE.build_path), "")
)
# The header text covers include order
checksum = pch_checksum(
src_dir,
pch_includes,
(
pch_text,
str(paths.framework),
str(paths.toolchain),
flags_id,
),
)
except (OSError, UnicodeError) as err:
# Identity unknown: a stale cache entry must never be served
_LOGGER.warning(
"Could not establish the pch identity; compiling without it: %s", err
)
else:
_LOGGER.info(
"Compiling with a precompiled header "
"(set ESPHOME_PCH_ENABLE=0 to disable)"
)
write_file_if_changed(pch_header, pch_text)
sum_path = build_dir / f"{PCH_HEADER_NAME}.gch.sum"
if checksum is not None:
# Generate-time stamp: a hand-run ninja can rebuild the .gch
# while this .sum lags
write_file_if_changed(sum_path, checksum + "\n")
else:
# A stale .sum from an earlier ccache run must not survive
sum_path.unlink(missing_ok=True)
gch = _e(f"{PCH_HEADER_NAME}.gch")
lines.append(f"build {gch}: pch {_e(pch_header)}")
if src_other:
lines.append(f" flags = {' '.join(src_other)}")
# Relative -include: absolute would break cross-device ccache.
# -Wno-error keeps a rejected .gch a warning under user -Werror
cxx_parts = src_other + [
f"-Winvalid-pch -Wno-error=invalid-pch -include {PCH_HEADER_NAME}"
]
lines.append(f"srccxxflags = {' '.join(cxx_parts)}")
src_cxx_override = ("$srccxxflags", gch)
mark_pch_emitted()
src_objs = _ninja_compile_edges(
lines,
_collect_sources(src_dir),
src_dir,
"src",
flags="$srcflags",
cxx_override=src_cxx_override,
)
ld_deps = [f"ld/{_COMMON_LD_NAME}"]
if CORE.testing_mode:
ld_deps.append(f"ld/{flash_ld}")
lines.append(
f"build firmware.elf: link {' '.join(src_objs + direct_objs)} | "
f"{' '.join(_e(a) for a in archives)} {' '.join(_e(d) for d in ld_deps)}"
)
lines.append(f" archives = {' '.join(_shell_token(a) for a in archives)}")
lines.append("build firmware.bin: elf2bin firmware.elf")
lines.append("build firmware.factory.bin: copy firmware.bin")
lines.append("build firmware.ota.bin: copy firmware.bin")
lines.append("default firmware.factory.bin firmware.ota.bin")
lines.append("")
return write_file_if_changed(build_dir / "build.ninja", "\n".join(lines))
def get_flash_ld_path(build_dir: Path, paths: InstalledPaths) -> Path:
"""The flash linker script the link actually uses (for size reporting).
Reads the same install the ninja file linked against instead of
re-resolving the framework version. A user-shipped override living in a
custom -L dir resolves to a nonexistent path here; the size consumer
warns and skips the Flash summary then.
"""
name = _active_flash_ld_name(_flash_ld_name(CORE.data[KEY_ESP8266][KEY_BOARD]))
if CORE.testing_mode:
return build_dir / "ld" / name
return paths.framework / "tools" / "sdk" / "ld" / name
def _flash_size_str(flash_size: int) -> str:
"""Flash size argument for elf2bin (e.g. ``4M``, ``512K``)."""
mb = 1024 * 1024
return f"{flash_size // mb}M" if flash_size >= mb else f"{flash_size // 1024}K"
+14 -1
View File
@@ -87,6 +87,19 @@ def ccache_defaults_env(cache_dir: Path) -> dict[str, str]:
"CCACHE_DIR": str(cache_dir),
"CCACHE_NOHASHDIR": "true",
"CCACHE_DEPEND": "1",
"CCACHE_BASEDIR": str(Path(CORE.build_path).resolve()),
# A user value wins via the filter below
"CCACHE_BASEDIR": effective_ccache_basedir(),
}
return {k: v for k, v in defaults.items() if k not in os.environ}
def effective_ccache_basedir() -> str:
"""The prefix ccache rewrites out of hashed paths: a user CCACHE_BASEDIR
wins, else the resolved build path (matching ccache_defaults_env)."""
from esphome.core import CORE
raw = os.environ.get("CCACHE_BASEDIR")
if raw is not None and Path(raw).is_absolute() and len(Path(raw).parts) > 1:
return raw
# Unset or degenerate ("", "/", relative): fall back to the build path
return str(Path(CORE.build_path).resolve())
+61 -6
View File
@@ -11,6 +11,7 @@ consumers (IDE integration, clang-tidy) expect:
from __future__ import annotations
from collections.abc import Callable
import json
import logging
import os
@@ -21,6 +22,8 @@ import subprocess
from esphome.core import EsphomeError
from esphome.helpers import write_file
_LOGGER = logging.getLogger(__name__)
# Everything idedata generation may raise after a successful link; idedata
# is a bonus artifact, so consumers warn instead of failing the build
IDEDATA_BEST_EFFORT_ERRORS = (
@@ -31,13 +34,36 @@ IDEDATA_BEST_EFFORT_ERRORS = (
ValueError,
)
_LOGGER = logging.getLogger(__name__)
# C++ translation-unit suffixes used to identify ESPHome source files.
_CXX_SUFFIXES = (".cpp", ".cc")
def warn_if_idedata_missing(get_idedata: Callable[[], dict | None]) -> None:
"""Run an idedata generator, downgrading any failure to a warning.
Shared by the native backends: the firmware already built, so a missing
or broken idedata must not fail a successful build.
"""
try:
if get_idedata() is None:
_LOGGER.warning("No idedata was generated for this build")
except IDEDATA_BEST_EFFORT_ERRORS as err:
_LOGGER.warning(
"Could not generate idedata: %s (IDE, clang-tidy, and "
"memory-analysis data will be unavailable for this build)",
err,
)
if isinstance(err, (EsphomeError, OSError)):
# Routine environmental failures keep the detail at debug
_LOGGER.debug("Idedata failure detail", exc_info=True)
else:
# LookupError/ValueError/RuntimeError smell like a parsing bug;
# a permanently masked traceback would hide it on every build
_LOGGER.warning("Idedata failure detail", exc_info=True)
# C++ translation-unit suffixes.
CXX_SOURCE_SUFFIXES = (".cpp", ".cc", ".cxx")
# Suffixes of input/output files that appear bare on the command line (and so
# must not be mistaken for compiler flags).
_INPUT_FILE_SUFFIXES = (*_CXX_SUFFIXES, ".c", ".o", ".S", ".s")
_INPUT_FILE_SUFFIXES = (*CXX_SOURCE_SUFFIXES, ".c", ".o", ".S", ".s")
# Path marker identifying an ESPHome source translation unit.
_ESPHOME_SRC_MARKER = "/src/esphome/"
@@ -46,7 +72,7 @@ def _is_esphome_src(file: str) -> bool:
"""Whether ``file`` is an ESPHome C++ translation unit; normalized to
``/`` first since Windows compile DBs use backslashes."""
return _ESPHOME_SRC_MARKER in file.replace("\\", "/") and file.endswith(
_CXX_SUFFIXES
CXX_SOURCE_SUFFIXES
)
@@ -121,7 +147,7 @@ def _pick_entry(entries: list[dict]) -> dict:
if _is_esphome_src(entry["file"]):
return entry
for entry in entries:
if entry["file"].endswith(_CXX_SUFFIXES):
if entry["file"].endswith(CXX_SOURCE_SUFFIXES):
return entry
raise ValueError("no C++ translation unit found in compile_commands.json")
@@ -135,6 +161,15 @@ def _is_launcher(token: str) -> bool:
return Path(token).stem.lower() in _LAUNCHER_STEMS
def is_joined_include(tok: str) -> bool:
"""The joined ``-includefoo.h`` spelling; excludes clang's -include-pch."""
return (
tok.startswith("-include")
and tok != "-include"
and not tok.startswith("-include-")
)
def parse_entry(
entry: dict, launcher: str | None = None
) -> tuple[str, list[str], list[str], list[str]]:
@@ -169,11 +204,23 @@ def parse_entry(
defines: list[str] = []
includes: list[str] = []
cxx_flags: list[str] = []
unresolved_force_includes: list[str] = []
it = iter(tokens[1:])
for tok in it:
if tok in ("-c", "-o"):
next(it, None) # drop the flag and its argument (input/output)
elif tok == "-include" or is_joined_include(tok):
# Re-anchor only names next to the compile (the pch); a name
# meant for the -I chain must stay untouched
raw = next(it, "") if tok == "-include" else tok[len("-include") :]
if not raw:
_LOGGER.warning("Dropping -include with no argument")
elif Path(resolved := _include(raw)).is_file():
cxx_flags.extend(("-include", resolved))
else:
unresolved_force_includes.append(raw)
cxx_flags.extend(("-include", raw))
elif tok.startswith("-D"):
# ``.strip()`` handles tokens like ``-D CONFIGURED=1`` (a single
# quoted arg with a space after -D) that some flags arrive as.
@@ -192,6 +239,14 @@ def parse_entry(
pass # input/output files
else:
cxx_flags.append(tok)
for raw in unresolved_force_includes:
# A deleted build artifact would otherwise surface only downstream
if not any((Path(inc) / raw).is_file() for inc in includes):
_LOGGER.warning(
"-include %s found neither next to the compile nor on the "
"include path; cached idedata may not resolve it",
raw,
)
return cxx_path, defines, includes, cxx_flags
+178
View File
@@ -0,0 +1,178 @@
"""Shared precompiled-header policy for the build backends.
The prefix either mirrors the TUs' own force-includes (ESP8266) or is a
curated core-header set (ESP-IDF). ``esphome: includes:`` sources receive
it too; Arduino.h visibility there is intended (esphome#8693).
"""
from __future__ import annotations
from collections.abc import Iterable
from dataclasses import dataclass
import hashlib
import logging
import os
from pathlib import Path
import posixpath
import re
import stat
from esphome.build_helpers.ccache import parse_enable_env
_DOMAIN = "pch"
@dataclass
class _PCHData:
emitted: bool = False
def _pch_data() -> _PCHData:
from esphome.core import CORE
if _DOMAIN not in CORE.data:
CORE.data[_DOMAIN] = _PCHData()
return CORE.data[_DOMAIN]
def mark_pch_emitted() -> None:
"""Record that this build's consumers reference the pch."""
_pch_data().emitted = True
_LOGGER = logging.getLogger(__name__)
# The header and its .gch/.sum sidecars live in the build directory.
PCH_HEADER_NAME = "esphome_pch.h"
# Every artifact the pch machinery can leave behind, for cleanup.
PCH_ARTIFACT_NAMES = (
PCH_HEADER_NAME,
f"{PCH_HEADER_NAME}.gch",
f"{PCH_HEADER_NAME}.gch.sum",
f"{PCH_HEADER_NAME}.gch.failed",
)
# The core defines header every backend anchors its prefix on.
PCH_CORE_HEADER = "esphome/core/defines.h"
# ccache cannot hash through a .gch; CCACHE_PCH_EXTSUM makes it hash the
# .sum sidecar instead of the .gch bytes, which are not reproducible.
# Keep in sync with the literals in platformio/pch.py.script.
_CCACHE_PCH_ENV = {
"CCACHE_SLOPPINESS": "pch_defines,time_macros",
"CCACHE_PCH_EXTSUM": "true",
}
# Both include forms: an angle include resolving under src/ must enter the
# digest too; ones that do not resolve simply end the walk
_INCLUDE_RE = re.compile(rb'^\s*#\s*include\s+["<]([^">]+)[">]', re.MULTILINE)
def pch_enabled() -> bool:
"""Precompiled-header knob: default on, ``ESPHOME_PCH_ENABLE=0`` opts out."""
return parse_enable_env("ESPHOME_PCH_ENABLE") is not False
def ccache_pch_env() -> dict[str, str]:
"""Settings ccache needs to cache compiles that consume the .gch;
empty unless this build actually emitted one. User-set values win.
Native backends export these process-wide; only time_macros affects
non-pch TUs."""
if not (pch_enabled() and _pch_data().emitted):
return {}
extsum = os.environ.get("CCACHE_PCH_EXTSUM")
if extsum is not None and extsum.strip().lower() not in ("1", "true", "yes", "on"):
# ccache then hashes the non-reproducible .gch bytes: permanent misses
_LOGGER.warning("CCACHE_PCH_EXTSUM=%s disables pch caching", extsum)
env = {k: v for k, v in _CCACHE_PCH_ENV.items() if k not in os.environ}
user_sloppiness = os.environ.get("CCACHE_SLOPPINESS")
if user_sloppiness is not None and (
missing := [
t
for t in ("pch_defines", "time_macros")
if t not in {tok.strip() for tok in user_sloppiness.split(",")}
]
):
# Without these ccache declines every pch-consuming compile
env["CCACHE_SLOPPINESS"] = ",".join((user_sloppiness, *missing))
_LOGGER.warning(
"Adding %s to CCACHE_SLOPPINESS so ccache can cache compiles "
"that use the precompiled header",
",".join(missing),
)
return env
def pch_extra_scripts() -> list[str]:
"""The extra_scripts entries a PlatformIO platform registers for the
pch; empty when disabled (the script itself has no enable check)."""
return ["post:pch.py"] if pch_enabled() else []
def pch_header_text(include_headers: Iterable[str]) -> str:
"""The prefix-header source: exactly these includes, in order."""
return "".join(f'#include "{name}"\n' for name in include_headers)
def _resolves(path: Path) -> bool:
"""False when missing; other stat failures propagate (identity unknown,
unlike is_file(), which would silently drop the header)."""
try:
return stat.S_ISREG(path.stat().st_mode)
except (FileNotFoundError, NotADirectoryError):
return False
def _include_closure(src_dir: Path, roots: Iterable[str]) -> dict[str, bytes]:
"""Include closure of ``roots``: src-relative name -> contents.
Resolution mirrors the compiler (includer's dir, then src root); names
outside ``src_dir`` end the walk and are versioned by the caller. No
#ifdef evaluation: over-approximating is the safe direction.
"""
seen: dict[str, bytes] = {}
stack: list[tuple[str, str]] = [(name, "") for name in roots]
while stack:
name, from_dir = stack.pop()
for candidate in (f"{from_dir}/{name}" if from_dir else name, name):
rel = posixpath.normpath(candidate)
if not rel.startswith("..") and _resolves(src_dir / rel):
break
else:
continue
if rel in seen:
continue
try:
data = (src_dir / rel).read_bytes()
except OSError as err:
# A marker would truncate the transitive walk; fail closed
_LOGGER.warning("Could not read %s for the pch checksum: %s", rel, err)
raise
seen[rel] = data
parent = posixpath.dirname(rel)
stack.extend(
# surrogateescape: a non-UTF-8 name just fails to resolve
(inc.decode(errors="surrogateescape"), parent)
for inc in _INCLUDE_RE.findall(data)
)
return seen
def pch_checksum(
src_dir: Path, include_headers: Iterable[str], extra: Iterable[str]
) -> str:
"""Digest standing in for the .gch in ccache's hash: the include closure
of the prefix header plus caller-supplied identity strings (versioned
install paths, flags). Raises OSError when a header's identity cannot
be established at all; callers must then compile without a pch."""
digest = hashlib.sha256()
closure = _include_closure(src_dir, include_headers)
for name in sorted(closure):
digest.update(name.encode(errors="surrogateescape"))
digest.update(closure[name])
digest.update(b"\0")
for item in extra:
digest.update(item.encode(errors="surrogateescape"))
digest.update(b"\0")
return digest.hexdigest()
+191 -41
View File
@@ -3,8 +3,10 @@ from pathlib import Path
import platform
import re
import subprocess
import time
from typing import Any
from esphome.build_helpers.pch import pch_extra_scripts
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
@@ -14,6 +16,7 @@ from esphome.const import (
CONF_FRAMEWORK,
CONF_PLATFORM_VERSION,
CONF_SOURCE,
CONF_TOOLCHAIN,
CONF_VERSION,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
@@ -21,6 +24,7 @@ from esphome.const import (
KEY_TARGET_PLATFORM,
PLATFORM_ESP8266,
ThreadModel,
Toolchain,
)
from esphome.core import (
CORE,
@@ -31,12 +35,13 @@ from esphome.core import (
)
from esphome.core.config import BOARD_MAX_LENGTH
from esphome.helpers import IS_MACOS, copy_file_if_changed
from esphome.platformio.toolchain import copy_ccache_script
from esphome.platformio.toolchain import copy_ccache_script, copy_pch_script
from esphome.storage_json import StorageJSON
from esphome.types import ConfigType
from .boards import BOARDS, board_ld_script
from .boards import BOARDS, ESP8266_BOARD_BUILD, board_ld_script
from .const import (
BUILD_FLASH_MODES,
CONF_EARLY_PIN_INIT,
CONF_ENABLE_SERIAL,
CONF_ENABLE_SERIAL1,
@@ -44,6 +49,7 @@ from .const import (
KEY_BOARD,
KEY_ESP8266,
KEY_PIN_INITIAL_STATES,
KEY_SCANF_FLOAT,
KEY_SERIAL1_REQUIRED,
KEY_SERIAL_REQUIRED,
KEY_WAVEFORM_REQUIRED,
@@ -103,6 +109,53 @@ def set_core_data(config: ConfigType) -> ConfigType:
return config
_TOOLCHAINS = (Toolchain.PLATFORMIO, Toolchain.ARDUINO)
_validate_toolchain = cv.toolchain_enum(_TOOLCHAINS)
_resolve_toolchain = cv.resolve_toolchain("ESP8266", _TOOLCHAINS, Toolchain.PLATFORMIO)
def _validate_native_toolchain(config: ConfigType) -> ConfigType:
"""Constraints of the native (non-PlatformIO) Arduino toolchain."""
if not CORE.using_toolchain_arduino:
return config
from esphome.arduino8266.framework import MIN_FRAMEWORK_VERSION
conf = config[CONF_FRAMEWORK]
version = cv.Version.parse(conf[CONF_VERSION])
if version < MIN_FRAMEWORK_VERSION:
raise cv.Invalid(
"'toolchain: arduino' requires framework version "
f"{MIN_FRAMEWORK_VERSION} or newer"
)
# platform_version is a PlatformIO concept; drop it (as esp32's native
# toolchain does), warning when a custom pin is discarded. The floor
# above guarantees the schema-derived default is the ARDUINO_4 spec.
if (
conf.pop(CONF_PLATFORM_VERSION, _ARDUINO_4_PLATFORM_SPEC)
!= _ARDUINO_4_PLATFORM_SPEC
):
_LOGGER.warning(
"'platform_version' is ignored by 'toolchain: arduino'; the native "
"toolchain downloads the framework and compiler directly"
)
if conf[CONF_SOURCE] != _format_framework_arduino_version(version):
raise cv.Invalid(
"'toolchain: arduino' does not support a custom framework source; "
"use 'toolchain: platformio'"
)
# BOARDS is a subset of ESP8266_BOARD_BUILD today; the second clause is
# a drift guard for the independently regenerated tables
if (
config[CONF_BOARD] not in BOARDS
or config[CONF_BOARD] not in ESP8266_BOARD_BUILD
):
raise cv.Invalid(
f"Board '{config[CONF_BOARD]}' is not supported by "
"'toolchain: arduino'; use 'toolchain: platformio'"
)
return config
def get_download_types(storage_json: StorageJSON) -> list[dict[str, str]]:
"""Binary-download entries for a built ESP8266 firmware.
@@ -132,7 +185,7 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
# a PIO platformio/framework-arduinoespressif8266 value
# List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif8266
# Same encoding the native toolchain uses for its package download, so a
# version bump cannot drift between the two paths.
# custom-source check against this value cannot drift from what it fetches.
from esphome.arduino8266.framework import framework_package_version
try:
@@ -194,7 +247,7 @@ def _arduino_check_versions(value: ConfigType) -> ConfigType:
platform_version = value.get(CONF_PLATFORM_VERSION)
if platform_version is None:
if version >= cv.Version(3, 1, 0):
platform_version = _parse_platform_version(str(ARDUINO_4_PLATFORM_VERSION))
platform_version = _ARDUINO_4_PLATFORM_SPEC
else:
platform_version = _parse_platform_version(str(ARDUINO_3_PLATFORM_VERSION))
value[CONF_PLATFORM_VERSION] = platform_version
@@ -217,6 +270,10 @@ def _parse_platform_version(value: Any) -> str:
return value
# The platform_version derived for every core >= 3.1.0 config
_ARDUINO_4_PLATFORM_SPEC = _parse_platform_version(str(ARDUINO_4_PLATFORM_VERSION))
ARDUINO_FRAMEWORK_SCHEMA = cv.All(
cv.Schema(
{
@@ -233,7 +290,6 @@ ARDUINO_FRAMEWORK_SCHEMA = cv.All(
)
BUILD_FLASH_MODES = ["qio", "qout", "dio", "dout"]
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
@@ -250,15 +306,30 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_ENABLE_SERIAL1): cv.boolean,
cv.Optional(CONF_ENABLE_FULL_PRINTF, default=False): cv.boolean,
cv.Optional(CONF_ENABLE_SCANF_FLOAT): cv.boolean,
cv.Optional(
CONF_TOOLCHAIN, visibility=cv.Visibility.ADVANCED
): _validate_toolchain,
}
),
# Until the native toolchain lands, PlatformIO is the only backend;
# reject a --toolchain this platform cannot serve yet.
cv.require_platformio_toolchain("ESP8266"),
_resolve_toolchain,
_validate_native_toolchain,
set_core_data,
)
def native_toolchain_module():
"""The native build backend for the resolved toolchain, if any.
``__main__`` dispatches from its own toolchain-keyed table; this helper
serves the component's internal callers.
"""
if not CORE.using_toolchain_arduino:
return None
from esphome.arduino8266 import toolchain
return toolchain
def check_rosetta() -> None:
"""Fail fast when the x86_64 ESP8266 toolchain cannot run on this Mac.
@@ -294,12 +365,13 @@ def _choose_ld_script(board: str) -> str:
@coroutine_with_priority(CoroPriority.PLATFORM)
async def to_code(config: ConfigType) -> None:
use_platformio = CORE.using_toolchain_platformio
cg.add(esp8266_ns.setup_preferences())
cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict")
cg.add_platformio_option("board", config[CONF_BOARD])
if use_platformio:
cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict")
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_build_flag("-DUSE_ESP8266")
cg.set_cpp_standard("gnu++20")
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
@@ -315,28 +387,33 @@ async def to_code(config: ConfigType) -> None:
"enabling scanf float support (~8KB flash)"
)
extra_scripts = [
"pre:ccache.py",
"pre:testing_mode.py",
"pre:exclude_updater.py",
"pre:exclude_waveform.py",
"pre:relocate_ratetable.py",
]
if not enable_scanf_float:
extra_scripts.append("pre:remove_float_scanf.py")
extra_scripts.append("post:post_build.py")
cg.add_platformio_option("extra_scripts", extra_scripts)
# The native generator reads the same decision (KEY_SCANF_FLOAT)
CORE.data[KEY_ESP8266][KEY_SCANF_FLOAT] = bool(enable_scanf_float)
if use_platformio:
extra_scripts = [
"pre:ccache.py",
"pre:testing_mode.py",
"pre:exclude_updater.py",
"pre:exclude_waveform.py",
"pre:relocate_ratetable.py",
]
if not enable_scanf_float:
extra_scripts.append("pre:remove_float_scanf.py")
extra_scripts.extend(pch_extra_scripts())
extra_scripts.append("post:post_build.py")
cg.add_platformio_option("extra_scripts", extra_scripts)
conf = config[CONF_FRAMEWORK]
cg.add_platformio_option("framework", "arduino")
cg.add_build_flag("-DUSE_ARDUINO")
cg.add_build_flag("-DUSE_ESP8266_FRAMEWORK_ARDUINO")
cg.add_build_flag("-Wno-nonnull-compare")
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
cg.add_platformio_option(
"platform_packages",
[f"platformio/framework-arduinoespressif8266@{conf[CONF_SOURCE]}"],
)
if use_platformio:
cg.add_platformio_option("framework", "arduino")
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
cg.add_platformio_option(
"platform_packages",
[f"platformio/framework-arduinoespressif8266@{conf[CONF_SOURCE]}"],
)
# Default for platformio is LWIP2_LOW_MEMORY with:
# - MSS=536
@@ -374,7 +451,8 @@ async def to_code(config: ConfigType) -> None:
# Force-include inline std::__throw_* overrides so GCC dead-strips the unused
# libstdc++ error message strings (e.g. "basic_string::_M_create") from DRAM.
# See throw_stubs.h for details. Must be prepended before <string>, so this
# uses build_src_flags with -include.
# uses build_src_flags with -include. Unconditional: the native build
# generator reads the same option, keeping one source of truth.
cg.add_platformio_option(
"build_src_flags", "-include esphome/components/esp8266/throw_stubs.h"
)
@@ -404,6 +482,8 @@ async def to_code(config: ConfigType) -> None:
# implementation in the Arduino ESP8266 core.
cg.add_build_flag("-Wl,--wrap=millis")
# Unconditional: the native build generator reads the same option,
# keeping one source of truth
cg.add_platformio_option("board_build.flash_mode", config[CONF_BOARD_FLASH_MODE])
ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
@@ -412,7 +492,7 @@ async def to_code(config: ConfigType) -> None:
cg.RawExpression(f"VERSION_CODE({ver.major}, {ver.minor}, {ver.patch})"),
)
if config[CONF_BOARD] in BOARDS:
if use_platformio and config[CONF_BOARD] in BOARDS:
cg.add_platformio_option(
"board_build.ldscript", _choose_ld_script(config[CONF_BOARD])
)
@@ -453,8 +533,24 @@ async def finalize_serial_config() -> None:
cg.add_build_flag("-DNO_GLOBAL_SERIAL1")
# Called by __main__.compile_program; returning False falls through to the
# PlatformIO toolchain.
def run_compile(args, config: ConfigType) -> bool:
# Positive check: the native backend only runs when explicitly resolved
toolchain = native_toolchain_module()
if toolchain is None:
return False
if toolchain.run_compile(config, CORE.verbose) != 0:
raise EsphomeError("ESP8266 native build failed")
return True
# Called by writer.py
def copy_files() -> None:
# Native builds skip the PlatformIO extra scripts; the build generator
# carries their logic
if CORE.using_toolchain_arduino:
return
dir = Path(__file__).parent
for script in (
"post_build",
@@ -469,6 +565,7 @@ def copy_files() -> None:
CORE.relative_build_path(f"{script}.py"),
)
copy_ccache_script()
copy_pch_script()
# ESP logs stack trace decoder, based on https://github.com/me-no-dev/EspExceptionDecoder
@@ -511,22 +608,75 @@ ESP8266_EXCEPTION_CODES = {
}
def _decode_pc(config: ConfigType, addr: str) -> None:
from esphome.platformio import toolchain
_DECODE_WARNED_AT: dict[str, float] = {}
idedata = toolchain.get_idedata(config)
if not idedata.addr2line_path or not idedata.firmware_elf_path:
_LOGGER.debug("decode_pc no addr2line")
return
command = [idedata.addr2line_path, "-pfiaC", "-e", idedata.firmware_elf_path, addr]
def _warn_decode_problem(key: str, message: str, *args) -> bool:
"""Warn, deduplicated briefly so a burst of stack-dump addresses warns
once but a later dump warns again; returns whether it warned so the
caller can mark suppressed addresses individually."""
now = time.monotonic()
last = _DECODE_WARNED_AT.get(key)
if last is not None and now - last < 30:
return False
_DECODE_WARNED_AT[key] = now
_LOGGER.warning(message, *args)
return True
def _decode_pc(config: ConfigType, addr: str, *, bulk: bool = False) -> None:
"""Decode one crash address. ``bulk``: the caller is scanning every
8-hex stack word, most of which are not code addresses -- unmappable
ones log at debug so real frames are not buried."""
if (native_toolchain := native_toolchain_module()) is not None:
addr2line = native_toolchain.get_addr2line_path()
elf = native_toolchain.get_elf_path()
for path in (addr2line, elf):
if not path.is_file():
_warn_decode_problem(
str(path), "Cannot decode crash addresses: %s missing", path
)
# The detailed warning names no address; mark named
# registers, but bulk stack words at debug (~150 per dump)
log = _LOGGER.debug if bulk else _LOGGER.warning
log("Not decoded %s (toolchain file missing)", addr)
return
addr2line, elf = str(addr2line), str(elf)
else:
from esphome.platformio import toolchain
idedata = toolchain.get_idedata(config)
if not idedata.addr2line_path or not idedata.firmware_elf_path:
_warn_decode_problem(
"no-addr2line",
"Cannot decode crash addresses: no addr2line or ELF in idedata",
)
log = _LOGGER.debug if bulk else _LOGGER.warning
log("Not decoded %s (no addr2line or ELF)", addr)
return
addr2line, elf = idedata.addr2line_path, idedata.firmware_elf_path
command = [addr2line, "-pfiaC", "-e", elf, addr]
try:
translation = subprocess.check_output(command, close_fds=False).decode().strip()
except Exception: # noqa: BLE001 # pylint: disable=broad-except
except Exception as err: # noqa: BLE001 # pylint: disable=broad-except
# Warn, not debug: a failing addr2line must be visible. The warning
# is rate-limited across a dump, so mark every undecoded address
# inline or the rest read as merely unmappable
if not _warn_decode_problem(
"addr2line-failed", "Could not decode crash address %s (%s)", addr, err
):
# The detailed warning already named this address; mark only
# the rate-limited ones, and bulk stack words at debug
log = _LOGGER.debug if bulk else _LOGGER.warning
log("Not decoded %s (addr2line failed)", addr)
_LOGGER.debug("Caught exception for command %s", command, exc_info=1)
return
if "?? ??:0" in translation:
# Nothing useful
# A named register that fails to decode is confusing silence; a
# bulk stack word failing is the expected common case
log = _LOGGER.debug if bulk else _LOGGER.warning
log("Not decoded %s (address not in %s)", addr, elf)
return
translation = translation.replace(" at ??:?", "").replace(":?", "")
_LOGGER.warning("Decoded %s", translation)
@@ -596,6 +746,6 @@ def process_stacktrace(config: ConfigType, line: str, backtrace_state: bool) ->
if backtrace_state:
for addr in re.finditer(STACKTRACE_ESP8266_BACKTRACE_PC_RE, line):
_decode_pc(config, addr.group())
_decode_pc(config, addr.group(), bulk=True)
return backtrace_state
+3 -1
View File
@@ -16,7 +16,6 @@ KEY_WAVEFORM_REQUIRED = "waveform_required"
KEY_SERIAL_REQUIRED = "serial_required"
KEY_SERIAL1_REQUIRED = "serial1_required"
# Set for the native (non-PlatformIO) toolchain's build generator
KEY_FLASH_MODE = "flash_mode"
KEY_SCANF_FLOAT = "scanf_float"
# Per-board flash-layout override consumed by board_ld_script()
KEY_LDSCRIPT = "ldscript"
@@ -73,3 +72,6 @@ def enable_serial1() -> None:
enable_serial1()
"""
CORE.data.setdefault(KEY_ESP8266, {})[KEY_SERIAL1_REQUIRED] = True
BUILD_FLASH_MODES = ("qio", "qout", "dio", "dout")
+8 -1
View File
@@ -555,7 +555,14 @@ NATIVE_ARDUINO_PIO_OPTIONS = frozenset({"board_build.f_cpu", "board_build.ldscri
# that is stored rather than translated away. Consumed by the esp8266 native
# backend (later in this chain) for its ignored-option warning; defined here
# so it stays adjacent to the routing.
NATIVE_ARDUINO_CONSUMED_PIO_OPTIONS = NATIVE_ARDUINO_PIO_OPTIONS | {"lib_ignore"}
# build_src_flags and board_build.flash_mode: set unconditionally by
# esp8266/__init__ and read by the native generator; not user-routable, so
# not in the set above
NATIVE_ARDUINO_CONSUMED_PIO_OPTIONS = NATIVE_ARDUINO_PIO_OPTIONS | {
"lib_ignore",
"build_src_flags",
"board_build.flash_mode",
}
@coroutine_with_priority(CoroPriority.FINAL)
+364
View File
@@ -0,0 +1,364 @@
import hashlib
import os
from pathlib import Path
import posixpath
import re
import shlex
import stat
import subprocess
import traceback
# pylint: disable=E0602
Import("env") # noqa: F821
_projenv_error = None
try:
Import("projenv") # noqa: F821
except Exception as err: # noqa: BLE001 -- not exported under -t nobuild
projenv = None
_projenv_error = err
# Precompile the src force-includes plus defines.h and force-include the
# result into C++ src compiles only; their preprocessed output is unchanged.
# Registration is gated host-side (pch_enabled()). Keep the closure, ccache
# values, and stamp flow in sync with build_helpers/pch.py.
# Compiler failures that clear on their own must not latch the .failed marker
_TRANSIENT_ERRORS = ("No space left", "Cannot allocate", "Resource temporarily")
_INCLUDE_RE = re.compile(rb'^\s*#\s*include\s+["<]([^">]+)[">]', re.MULTILINE)
_CORE_HEADER = "esphome/core/defines.h"
def _raise_walk_error(err: OSError) -> None:
raise err
def _resolves_dir(path: Path) -> bool:
"""False when missing; other stat failures propagate."""
try:
return stat.S_ISDIR(path.stat().st_mode)
except (FileNotFoundError, NotADirectoryError):
return False
def _resolves(path: Path) -> bool:
"""False when missing; other stat failures propagate (identity unknown)."""
try:
return stat.S_ISREG(path.stat().st_mode)
except (FileNotFoundError, NotADirectoryError):
return False
def _include_closure(src_dir: Path, roots: list) -> dict:
"""Quoted-include closure: src-relative name -> contents (mirror of
build_helpers/pch.py)."""
seen = {}
stack = [(name, "") for name in roots]
while stack:
name, from_dir = stack.pop()
for candidate in (f"{from_dir}/{name}" if from_dir else name, name):
rel = posixpath.normpath(candidate)
if not rel.startswith("..") and _resolves(src_dir / rel):
break
else:
continue
if rel in seen:
continue
try:
data = (src_dir / rel).read_bytes()
except OSError as err:
# A marker would truncate the transitive walk; fail closed
print(f"ESPHome: could not read {rel} for the pch checksum: {err}")
raise
seen[rel] = data
parent = posixpath.dirname(rel)
stack.extend(
(inc.decode(errors="surrogateescape"), parent)
for inc in _INCLUDE_RE.findall(data)
)
return seen
def _shell_arg(element) -> str | None:
"""One compiler argv from one SCons element, matching the real spawn:
spaced elements pass whole, the rest get one shell unquote (skipped on
Windows, where shlex would eat path backslashes)."""
arg = str(element)
if " " in arg or os.name == "nt":
return arg.replace('\\"', '"')
if not arg.strip():
return arg
try:
tokens = shlex.split(arg)
except ValueError as err:
print(f"ESPHome: could not lex flag {arg!r} for the pch: {err}")
return None
if len(tokens) != 1:
# A flag the model cannot reproduce would diverge the .gch's flags
print(f"ESPHome: cannot model flag {arg!r} for the pch")
return None
return tokens[0]
def _compile_gch(cxx, flags, header: Path, gch: Path, proj_dir: Path):
"""Compile the .gch, then probe that the toolchain can load it back
(GCC 10 on macOS arm64 rejects its own per-process). Returns an error
string or None; OSError propagates as transient."""
result = subprocess.run( # noqa: PLW1510
[cxx, "-x", "c++-header", *flags, "-c", str(header), "-o", str(gch)],
cwd=proj_dir,
# C locale keeps diagnostics matchable by _TRANSIENT_ERRORS
env={**os.environ, "LC_ALL": "C"},
capture_output=True,
text=True,
)
if result.returncode < 0:
# Signal-killed (OOM, ^C): route to the transient no-marker path
raise OSError(f"compiler killed by signal {-result.returncode}")
if result.returncode != 0:
return result.stderr
# -MF is only legal alongside a dependency flag; pass it solely to
# redirect a depfile that -MD/-MMD in the flags would otherwise write
dep_redirect = (
["-MF", os.devnull]
if any(f in ("-MD", "-MMD", "-M", "-MM") for f in flags)
else []
)
probe = subprocess.run( # noqa: PLW1510
[
cxx,
*flags,
*dep_redirect,
"-Winvalid-pch",
"-include",
str(header),
"-fsyntax-only",
"-x",
"c++",
"-",
],
cwd=proj_dir,
env={**os.environ, "LC_ALL": "C"},
input="",
capture_output=True,
text=True,
)
if probe.returncode < 0:
raise OSError(f"probe killed by signal {-probe.returncode}")
if probe.returncode != 0 or ".gch" in probe.stderr:
return f"toolchain cannot load the pch: {probe.stderr.strip()}"
return None
def _read_stamp(path: Path) -> str:
"""A corrupt sidecar must read as stale, not kill the pch forever."""
try:
return path.read_text(encoding="utf-8").strip()
except (OSError, UnicodeDecodeError):
return ""
def _setup_pch() -> None:
if projenv is None:
# Expected under -t nobuild; anything else must leave a trail
print(f"ESPHome: projenv unavailable ({_projenv_error}); skipping pch")
return
# Project root: SCons compiles run here, so the relative -include
# resolves; an absolute path would break cross-device ccache sharing.
proj_dir = Path(env.subst("$PROJECT_DIR")) # noqa: F821
src_dir = Path(env.subst("$PROJECT_SRC_DIR")) # noqa: F821
header = proj_dir / "esphome_pch.h"
gch = Path(f"{header}.gch")
sum_path = Path(f"{gch}.sum")
cxx = projenv.subst("$CXX") # noqa: F821
# The header holds the -include entries itself, so the .gch compile must
# not see them; consumers keep theirs, which the .gch then satisfies.
flags = []
include_headers = []
raw_args = [
_shell_arg(element)
for element in projenv.subst_list("$CXXFLAGS $CCFLAGS $_CCCOMCOM")[0] # noqa: F821
]
if any(arg is None for arg in raw_args):
print("ESPHome: skipping precompiled header: unmodelable flag")
return
flag_it = iter(raw_args)
for tok in flag_it:
if tok == "-include":
include_headers.append(next(flag_it, ""))
elif tok.startswith("-include") and not tok.startswith("-include-"):
include_headers.append(tok[len("-include") :])
else:
flags.append(tok)
if any(not name for name in include_headers):
print("ESPHome: build_src_flags has a trailing -include; skipping pch")
return
# Fold only relative names resolving under src/: consumers keep their
# own -include entries, so folding an unguarded user header would
# include it twice; unfolded ones stay consumer-only.
try:
folded = [
name
for name in include_headers
if not Path(name).is_absolute() and _resolves(src_dir / name)
]
except OSError as err:
print(f"ESPHome: skipping precompiled header: {err}")
return
if unfolded := [n for n in include_headers if n not in folded]:
print(f"ESPHome: not precompiling non-src force-includes: {unfolded}")
content = "".join(f'#include "{name}"\n' for name in (*folded, _CORE_HEADER))
digest = hashlib.sha256()
digest.update(content.encode(errors="surrogateescape"))
digest.update(cxx.encode(errors="surrogateescape"))
# Mirror CCACHE_BASEDIR: strip the per-device build path so identical
# configs produce identical .sum files and share cache entries
flags_id = " ".join(flags)
if basedir := os.environ.get("CCACHE_BASEDIR"):
flags_id = flags_id.replace(basedir, "")
digest.update(flags_id.encode(errors="surrogateescape"))
# GCC never validates a .gch against its source headers, and PlatformIO
# package paths carry no version, so a package bump must invalidate here
platform = env.PioPlatform() # noqa: F821
for package in sorted(platform.packages):
try:
version = platform.get_package_version(package)
except KeyError:
# An unresolved manifest must not hash as a constant
if platform.get_package(package) is not None:
print(f"ESPHome: skipping precompiled header: no version for {package}")
return
version = None # absent optional package
except Exception as err: # noqa: BLE001
# No trustworthy package identity: a stale .gch could survive
print(f"ESPHome: skipping precompiled header: {err}")
return
digest.update(f"{package}={version}".encode())
digest.update(b"\0")
try:
closure = _include_closure(src_dir, [*folded, _CORE_HEADER])
except OSError as err:
print(f"ESPHome: skipping precompiled header: {err}")
return
for rel in sorted(closure):
digest.update(rel.encode(errors="surrogateescape"))
digest.update(closure[rel])
digest.update(b"\0")
# Project-local -I dirs (e.g. rp2's lwip_override) hold generated
# headers the src closure cannot see; hash them too
prev = ""
try:
for tok in flags:
inc = tok[2:] if tok.startswith("-I") and len(tok) > 2 else ""
if prev == "-I":
inc = tok
prev = tok
if not inc:
continue
inc_dir = Path(inc)
if not (
_resolves_dir(inc_dir)
and inc_dir.is_relative_to(proj_dir)
and not inc_dir.is_relative_to(src_dir)
# Library trees never enter the prefix closure; walking them
# would read every library file each build
and not inc_dir.is_relative_to(proj_dir / ".piolibdeps")
and not inc_dir.is_relative_to(proj_dir / ".pioenvs")
):
continue
local_headers = []
# os.walk with onerror: rglob would swallow unlistable subtrees
for root, _dirs, files in os.walk(inc_dir, onerror=_raise_walk_error):
local_headers.extend(
Path(root) / f
for f in files
if f.endswith((".h", ".hpp", ".hh", ".inc"))
)
for local in sorted(local_headers):
digest.update(str(local.relative_to(proj_dir)).encode())
digest.update(local.read_bytes())
digest.update(b"\0")
except OSError as err:
print(f"ESPHome: skipping precompiled header: {err}")
return
checksum = digest.hexdigest()
# The ccache .sum sidecar doubles as the freshness stamp
if (
not header.is_file()
or not gch.is_file()
or not sum_path.is_file()
or (_read_stamp(sum_path) != checksum)
):
failed_marker = Path(f"{gch}.failed")
if _read_stamp(failed_marker) == checksum:
print(
"ESPHome: skipping precompiled header (previous attempt "
f"failed); delete {failed_marker.name} to retry"
)
return
header.write_text(content, encoding="utf-8")
try:
error = _compile_gch(cxx, flags, header, gch, proj_dir)
except OSError as err:
# Transient spawn/IO failure: no marker, retry next build
print(f"ESPHome: precompiled header compile did not run: {err}")
gch.unlink(missing_ok=True)
sum_path.unlink(missing_ok=True)
return
if error is not None:
print("ESPHome: precompiled header failed; compiling without it")
print(error)
gch.unlink(missing_ok=True)
sum_path.unlink(missing_ok=True)
if any(m in error for m in _TRANSIENT_ERRORS):
# Resource exhaustion clears on its own; retry next build
return
# Skip retries until a flag/header/platform change alters the checksum
failed_marker.write_text(checksum + "\n", encoding="utf-8")
return
failed_marker.unlink(missing_ok=True)
sum_path.write_text(checksum + "\n", encoding="utf-8")
# Computed first so the flags and env land together: a raise between
# them would leave a pch-consuming build without its ccache settings.
# projenv["ENV"] aliases os.environ, so these reach all TUs; only
# time_macros affects non-pch TUs. User values win.
ccache_updates = {
key: value
for key, value in (
("CCACHE_SLOPPINESS", "pch_defines,time_macros"),
("CCACHE_PCH_EXTSUM", "true"),
)
if key not in os.environ
}
sloppiness = os.environ.get("CCACHE_SLOPPINESS")
if sloppiness is not None:
tokens = {tok.strip() for tok in sloppiness.split(",")}
missing = [t for t in ("pch_defines", "time_macros") if t not in tokens]
if missing:
# Without these ccache declines every pch-consuming compile
ccache_updates["CCACHE_SLOPPINESS"] = ",".join((sloppiness, *missing))
print(f"ESPHome: adding {','.join(missing)} to CCACHE_SLOPPINESS for the pch")
extsum = os.environ.get("CCACHE_PCH_EXTSUM")
if extsum is not None and extsum.strip().lower() not in ("1", "true", "yes", "on"):
# ccache then hashes the non-reproducible .gch bytes: permanent misses
print(f"ESPHome: CCACHE_PCH_EXTSUM={extsum} disables pch caching")
# Prepended: GCC only uses a .gch while no other tokens precede it.
# The relative name also reaches "pio run -t idedata" output.
# -Wno-error: the per-process probe can pass while a later cc1plus
# rejects the .gch; that must stay a warning under user -Werror.
projenv.Prepend( # noqa: F821
CXXFLAGS=["-Winvalid-pch", "-Wno-error=invalid-pch", "-include", header.name]
)
projenv["ENV"].update(ccache_updates) # noqa: F821
print("ESPHome: Compiling with precompiled header")
try:
_setup_pch()
except Exception: # noqa: BLE001 -- a speedup must never break the build
print("ESPHome: pch internal error; compiling without it")
traceback.print_exc()
+9
View File
@@ -289,6 +289,15 @@ def copy_ccache_script() -> None:
)
def copy_pch_script() -> None:
"""Copy the shared precompiled-header SCons post-script into the build
dir; platform components pair it with ``post:pch.py`` in extra_scripts."""
copy_file_if_changed(
Path(__file__).parent / "pch.py.script",
CORE.relative_build_path("pch.py"),
)
def default_libdeps_dir() -> str:
"""The PLATFORMIO_LIBDEPS_DIR value a pio run defaults to; the package
prefetch must resolve installed libraries against the same dir."""
+14 -4
View File
@@ -7,6 +7,7 @@ import re
import time
from esphome import loader
from esphome.build_helpers.pch import PCH_ARTIFACT_NAMES
from esphome.compiled_config import save_compiled_config
from esphome.config import iter_component_configs, iter_components
from esphome.const import (
@@ -609,11 +610,20 @@ def clean_build(clear_pio_cache: bool = True, *, full: bool = False):
if idf_path.is_dir():
_LOGGER.info("Deleting %s", idf_path)
rmtree(idf_path)
# The PlatformIO pch artifacts live at the project root so the
# relative -include resolves; a partial clean must drop them too
for name in PCH_ARTIFACT_NAMES:
CORE.relative_build_path(name).unlink(missing_ok=True)
# The idedata cache is derived from the build but lives under the data dir,
# not the build path, so it must be removed separately in both modes.
idedata_cache = CORE.relative_internal_path("idedata", f"{CORE.name}.json")
if idedata_cache.is_file():
# The idedata caches are derived from the build but live under the data
# dir, not the build path, so they must be removed separately in both
# modes. Globbed (name.json plus name.<backend>.json) so a future
# backend suffix cannot silently drift out of clean-all.
idedata_dir = CORE.relative_internal_path("idedata")
for idedata_cache in (
*idedata_dir.glob(f"{CORE.name}.json"),
*idedata_dir.glob(f"{CORE.name}.*.json"),
):
_LOGGER.info("Deleting %s", idedata_cache)
idedata_cache.unlink()
+132 -24
View File
@@ -50,6 +50,7 @@ from __future__ import annotations
import argparse
from collections import Counter
from collections.abc import Callable
from enum import StrEnum
from functools import cache
import json
@@ -531,37 +532,53 @@ ESP32_PLATFORMIO_TRIGGER_PATH_PREFIXES = ("esphome/platformio/",)
# - esphome/build_gen/platformio.py -- the PlatformIO build generator
# - script/test_build_components.py -- the harness the job invokes
# - .github/workflows/ci.yml -- the job's own definition
ESP32_PLATFORMIO_TRIGGER_FILES = frozenset(
# Shared by every toolchain smoke-test job: the harness it invokes and the
# workflow that defines it
_SMOKE_HARNESS_TRIGGER_FILES = frozenset(
{
"esphome/build_gen/platformio.py",
"script/test_build_components.py",
".github/workflows/ci.yml",
}
)
ESP32_PLATFORMIO_TRIGGER_FILES = _SMOKE_HARNESS_TRIGGER_FILES | {
"esphome/build_gen/platformio.py",
}
def _path_or_file_trigger(
files: list[str],
trigger_files: frozenset[str],
trigger_prefixes: tuple[str, ...],
) -> bool:
"""Whether any changed file matches the given infrastructure triggers."""
return any(
file in trigger_files or file.startswith(trigger_prefixes) for file in files
)
@cache
def _cached_components_closure(files: tuple[str, ...]) -> frozenset[str]:
"""Dependency closure of the changed components, from the changed files.
The walk is expensive and every toolchain smoke-test job asks for the
same file list, so compute it once per run."""
component_files = [f for f in files if filter_component_and_test_files(f)]
return frozenset(get_components_with_dependencies(component_files, True))
def _esp32_platformio_path_or_file_trigger(files: list[str]) -> bool:
"""Whether any changed file is a PlatformIO infrastructure / harness trigger."""
for file in files:
if file in ESP32_PLATFORMIO_TRIGGER_FILES:
return True
if any(
file.startswith(prefix) for prefix in ESP32_PLATFORMIO_TRIGGER_PATH_PREFIXES
):
return True
return False
return _path_or_file_trigger(
files, ESP32_PLATFORMIO_TRIGGER_FILES, ESP32_PLATFORMIO_TRIGGER_PATH_PREFIXES
)
def _esp_idf_infra_changed(files: list[str]) -> bool:
"""Whether any changed file is ESP-IDF build/runner infrastructure."""
for file in files:
if file in ESP_IDF_INFRA_TRIGGER_FILES:
return True
if any(
file.startswith(prefix) for prefix in ESP_IDF_INFRA_TRIGGER_PATH_PREFIXES
):
return True
return False
return _path_or_file_trigger(
files, ESP_IDF_INFRA_TRIGGER_FILES, ESP_IDF_INFRA_TRIGGER_PATH_PREFIXES
)
def esp32_platformio_components_to_test(branch: str | None = None) -> list[str]:
@@ -599,15 +616,23 @@ def esp32_platformio_components_to_test(branch: str | None = None) -> list[str]:
Returns:
Sorted list of component names to compile.
"""
return _toolchain_components_to_test(
branch, ESP32_PLATFORMIO_TEST_COMPONENTS, _esp32_platformio_path_or_file_trigger
)
def _toolchain_components_to_test(
branch: str | None,
test_set: frozenset[str],
infra_trigger: Callable[[list[str]], bool],
) -> list[str]:
"""The shared narrowing rule for the per-toolchain smoke-test jobs."""
files = changed_files(branch)
if core_changed(files) or _esp32_platformio_path_or_file_trigger(files):
return sorted(ESP32_PLATFORMIO_TEST_COMPONENTS)
if core_changed(files) or infra_trigger(files):
return sorted(test_set)
component_files = [f for f in files if filter_component_and_test_files(f)]
changed = get_components_with_dependencies(component_files, True)
return sorted(ESP32_PLATFORMIO_TEST_COMPONENTS & set(changed))
return sorted(test_set & _cached_components_closure(tuple(files)))
def should_run_esp32_platformio(branch: str | None = None) -> bool:
@@ -628,6 +653,83 @@ def should_run_esp32_platformio(branch: str | None = None) -> bool:
return bool(esp32_platformio_components_to_test(branch))
# The `--toolchain arduino` smoke-test set: covers the core, the bundled and
# converted registry libraries, and the waveform path.
ESP8266_NATIVE_TEST_COMPONENTS = frozenset(
{
"esp8266",
"api",
"web_server",
"captive_portal",
"mqtt",
"esp8266_pwm",
"neopixelbus",
"bme280_i2c",
"uart",
}
)
# Infrastructure whose changes always trigger the native ESP8266 compile
# test. esphome/build_helpers/ holds the idedata and size-summary helpers
# the backend shares with the native ESP-IDF build.
ESP8266_NATIVE_TRIGGER_PATH_PREFIXES = (
"esphome/arduino8266/",
"esphome/arduino/",
"esphome/build_helpers/",
)
# Shared library-conversion modules every native build imports; espidf-only
# infra (build_gen/espidf.py) deliberately stays out of the esp8266 set.
_NATIVE_SHARED_TRIGGER_FILES = frozenset(
{
"esphome/framework_helpers.py",
"esphome/platformio/library.py",
"esphome/platformio/extra_script.py",
}
)
# Tripwire: the shared modules must stay in the ESP-IDF trigger set too
# (now defined in clang_tidy_hash), or its smoke test silently skips them
assert _NATIVE_SHARED_TRIGGER_FILES <= ESP_IDF_INFRA_TRIGGER_FILES
ESP8266_NATIVE_TRIGGER_FILES = (
_NATIVE_SHARED_TRIGGER_FILES
| _SMOKE_HARNESS_TRIGGER_FILES
| {
"esphome/build_gen/arduino8266.py",
"esphome/build_gen/build_tool.py",
"esphome/components/esp8266/build_surgery.py",
"esphome/components/esp8266/boards.py",
"esphome/platformio/registry.py",
# esp8266/__init__.py imports copy_ccache_script from it
"esphome/platformio/toolchain.py",
".github/actions/cache-arduino8266/action.yml",
}
)
def _esp8266_native_path_or_file_trigger(files: list[str]) -> bool:
"""Whether any changed file is native-ESP8266 infrastructure / harness."""
# base_python_changed covers the top-level esphome/*.py modules the
# native backend imports directly (framework_helpers, helpers, writer,
# __main__); without it a change there would silently skip this job.
# base_python_changed is deliberately broad (any top-level esphome/*.py)
# as belt-and-braces while the backend is new; narrow it to the modules
# the backend imports once the toolchain has soaked a few releases
return base_python_changed(files) or _path_or_file_trigger(
files, ESP8266_NATIVE_TRIGGER_FILES, ESP8266_NATIVE_TRIGGER_PATH_PREFIXES
)
def esp8266_native_components_to_test(branch: str | None = None) -> list[str]:
"""Subset of ``ESP8266_NATIVE_TEST_COMPONENTS`` the job needs to compile.
Same narrowing logic as ``esp32_platformio_components_to_test``: the full
list on core or infrastructure changes, otherwise the intersection with
the changed-component dependency closure (empty list skips the job).
"""
return _toolchain_components_to_test(
branch, ESP8266_NATIVE_TEST_COMPONENTS, _esp8266_native_path_or_file_trigger
)
def determine_cpp_unit_tests(
branch: str | None = None,
) -> tuple[bool, list[str]]:
@@ -1226,6 +1328,8 @@ def main() -> None:
run_device_builder = True
esp32_platformio_components = sorted(ESP32_PLATFORMIO_TEST_COMPONENTS)
run_esp32_platformio = True
esp8266_native_components = sorted(ESP8266_NATIVE_TEST_COMPONENTS)
run_esp8266_native = True
else:
integration_run_all, integration_test_files = determine_integration_tests(
args.branch
@@ -1237,6 +1341,8 @@ def main() -> None:
run_device_builder = should_run_device_builder(args.branch)
esp32_platformio_components = esp32_platformio_components_to_test(args.branch)
run_esp32_platformio = bool(esp32_platformio_components)
esp8266_native_components = esp8266_native_components_to_test(args.branch)
run_esp8266_native = bool(esp8266_native_components)
run_integration, integration_test_buckets = _compute_integration_test_buckets(
integration_run_all, integration_test_files
)
@@ -1432,6 +1538,8 @@ def main() -> None:
"device_builder": run_device_builder,
"esp32_platformio": run_esp32_platformio,
"esp32_platformio_components": ",".join(esp32_platformio_components),
"esp8266_native": run_esp8266_native,
"esp8266_native_components": ",".join(esp8266_native_components),
"changed_components": changed_components,
"changed_components_with_tests": changed_components_with_tests,
"directly_changed_components_with_tests": list(directly_changed_with_tests),
+50 -8
View File
@@ -1027,6 +1027,7 @@ def test_components(
isolated_components: set[str] | None = None,
base_only: bool = False,
toolchain: str | None = None,
fail_on_no_tests: bool = False,
) -> int:
"""Test components with optional intelligent grouping.
@@ -1061,20 +1062,34 @@ def test_components(
# toolchain build.
include_validate = esphome_command != "compile"
# Find all component tests
# Find all component tests; remember which components each pattern
# (wildcards included) matched, for the deferred no-tests accounting
all_tests = {}
pattern_components: dict[str, set[str]] = {}
for pattern in component_patterns:
# Skip empty patterns (happens when components list is empty string)
if not pattern:
continue
all_tests.update(
find_component_tests(
tests_dir, pattern, base_only, include_validate=include_validate
)
found = find_component_tests(
tests_dir, pattern, base_only, include_validate=include_validate
)
pattern_components[pattern] = set(found)
all_tests.update(found)
# The flag's contract is "no test matched fails": a fully blank pattern
# list would otherwise slide into the reference-baseline fallback and
# exit green while building nothing a caller asked for
if fail_on_no_tests and not any(component_patterns):
print("No components requested (blank component list)")
return 1
if fail_on_no_tests and not all_tests:
# Nothing matched at all: fail before the synthetic baseline build,
# which would spend a compile reporting success on nothing. Partial
# matches defer to the per-pattern accounting after the summary.
print(f"No components found matching: {component_patterns}")
return 1
# If no components found, build a reference configuration for baseline comparison
# Create a synthetic "empty" component test that will build just the base config
if not all_tests:
print(f"No components found matching: {component_patterns}")
print(
@@ -1178,6 +1193,26 @@ def test_components(
toolchain=toolchain,
)
silent: list[str] = []
if fail_on_no_tests:
# A green run that built nothing for a requested pattern (renamed
# fixture, missing base file, version-suffix mismatch, a wildcard
# matching no component) must not pass CI. Per pattern: an
# all-or-nothing check would let one silent pattern hide behind the
# others. Opt-in: some legs (the esp32-ard smoke subset)
# legitimately match nothing. Failing is deferred past the summary
# so a real failure's reproduce commands still print.
built = {c for r in test_results for c in r.components}
# A pattern is silent when it matched no fixture, or when none of
# its matched components produced a build (wildcards included)
silent = [
p
for p in component_patterns
if p and not (pattern_components.get(p, set()) & built)
]
if silent:
print(f"No tests ran for requested pattern(s): {', '.join(silent)}")
# Separate results into passed and failed
passed_results = [r for r in test_results if r.success]
failed_results = [r for r in test_results if not r.success]
@@ -1209,7 +1244,7 @@ def test_components(
if os.environ.get("GITHUB_STEP_SUMMARY"):
write_github_summary(test_results, toolchain=toolchain)
if failed_results:
if failed_results or silent:
return 1
return 0
@@ -1264,6 +1299,12 @@ def main() -> int:
"--toolchain",
help="Select toolchain for compiling.",
)
parser.add_argument(
"--fail-on-no-tests",
action="store_true",
help="Exit non-zero when no test matched (for CI legs whose "
"components must all have fixtures)",
)
args = parser.parse_args()
@@ -1282,6 +1323,7 @@ def main() -> int:
continue_on_fail=args.continue_on_fail,
enable_grouping=not args.no_grouping,
isolated_components=isolated_components,
fail_on_no_tests=args.fail_on_no_tests,
base_only=args.base_only,
toolchain=args.toolchain,
)
+92
View File
@@ -78,6 +78,17 @@ def mock_esp32_platformio_components_to_test() -> Generator[Mock, None, None]:
yield mock
@pytest.fixture
def mock_esp8266_native_components_to_test() -> Generator[Mock, None, None]:
"""Mock esp8266_native_components_to_test from determine_jobs.
main() drives both the ``esp8266_native`` boolean output and the
``esp8266_native_components`` CSV from this one function.
"""
with patch.object(determine_jobs, "esp8266_native_components_to_test") as mock:
yield mock
@pytest.fixture
def mock_determine_cpp_unit_tests() -> Generator[Mock, None, None]:
"""Mock determine_cpp_unit_tests from helpers."""
@@ -106,6 +117,7 @@ def clear_determine_jobs_caches() -> None:
"""Clear all cached functions before each test."""
determine_jobs._is_clang_tidy_full_scan.cache_clear()
determine_jobs._component_has_tests.cache_clear()
determine_jobs._cached_components_closure.cache_clear()
def test_main_all_tests_should_run(
@@ -116,6 +128,7 @@ def test_main_all_tests_should_run(
mock_should_run_import_time: Mock,
mock_should_run_device_builder: Mock,
mock_esp32_platformio_components_to_test: Mock,
mock_esp8266_native_components_to_test: Mock,
mock_changed_files: Mock,
mock_determine_cpp_unit_tests: Mock,
capsys: pytest.CaptureFixture[str],
@@ -132,6 +145,7 @@ def test_main_all_tests_should_run(
mock_should_run_import_time.return_value = True
mock_should_run_device_builder.return_value = True
mock_esp32_platformio_components_to_test.return_value = ["api", "esp32"]
mock_esp8266_native_components_to_test.return_value = ["api", "logger"]
mock_determine_cpp_unit_tests.return_value = (False, ["wifi", "api", "sensor"])
# Mock changed_files to return non-component files (to avoid memory impact)
@@ -208,6 +222,8 @@ def test_main_all_tests_should_run(
assert output["device_builder"] is True
assert output["esp32_platformio"] is True
assert output["esp32_platformio_components"] == "api,esp32"
assert output["esp8266_native"] is True
assert output["esp8266_native_components"] == "api,logger"
assert output["changed_components"] == ["wifi", "api", "sensor"]
# changed_components_with_tests will only include components that actually have test files
assert "changed_components_with_tests" in output
@@ -244,6 +260,7 @@ def test_main_no_tests_should_run(
mock_should_run_import_time: Mock,
mock_should_run_device_builder: Mock,
mock_esp32_platformio_components_to_test: Mock,
mock_esp8266_native_components_to_test: Mock,
mock_changed_files: Mock,
mock_determine_cpp_unit_tests: Mock,
capsys: pytest.CaptureFixture[str],
@@ -260,6 +277,7 @@ def test_main_no_tests_should_run(
mock_should_run_import_time.return_value = False
mock_should_run_device_builder.return_value = False
mock_esp32_platformio_components_to_test.return_value = []
mock_esp8266_native_components_to_test.return_value = []
mock_determine_cpp_unit_tests.return_value = (False, [])
# Mock changed_files to return no component files
@@ -302,6 +320,8 @@ def test_main_no_tests_should_run(
assert output["device_builder"] is False
assert output["esp32_platformio"] is False
assert output["esp32_platformio_components"] == ""
assert output["esp8266_native"] is False
assert output["esp8266_native_components"] == ""
assert output["changed_components"] == []
assert output["changed_components_with_tests"] == []
assert output["component_test_count"] == 0
@@ -3151,6 +3171,78 @@ def test_memory_impact_elf_layouts_are_found(tmp_path: Path) -> None:
assert find_elf_path(build_path) == elf, f"{platform} ELF not found"
@pytest.mark.parametrize(
"changed",
[
"esphome/arduino8266/framework.py",
"esphome/build_gen/arduino8266.py",
"esphome/components/esp8266/build_surgery.py",
# Shared modules the native build depends on
"esphome/build_helpers/idedata.py",
"esphome/platformio/library.py",
# Top-level esphome/*.py modules the backend imports directly
"esphome/framework_helpers.py",
"esphome/writer.py",
# esp8266/__init__.py imports copy_ccache_script from it
"esphome/platformio/toolchain.py",
# The composite cache action must not ship unexercised
".github/actions/cache-arduino8266/action.yml",
],
)
def test_esp8266_native_components_full_list_on_infra_change(changed: str) -> None:
"""Native-ESP8266 infrastructure changes run the full test list."""
with (
patch.object(determine_jobs, "changed_files", return_value=[changed]),
patch.object(
determine_jobs,
"get_components_with_dependencies",
return_value=["wifi"],
),
):
result = determine_jobs.esp8266_native_components_to_test()
assert result == sorted(determine_jobs.ESP8266_NATIVE_TEST_COMPONENTS)
@pytest.mark.parametrize(
("changed_files", "dependency_closure", "expected"),
[
# Tested component changed -- narrow to the intersection.
(
["esphome/components/mqtt/mqtt_client.cpp"],
["mqtt", "json"],
["mqtt"],
),
# Components outside the test set return an empty list (job skipped).
(
["esphome/components/wifi/wifi_component.cpp"],
["wifi", "network"],
[],
),
# espidf infrastructure is not an esp8266-native trigger; the
# native backend depends on esphome/build_helpers/ instead.
(["esphome/build_gen/espidf.py"], [], []),
(["esphome/espidf/toolchain.py"], [], []),
(["README.md"], [], []),
],
)
def test_esp8266_native_components_to_test_narrowing(
changed_files: list[str],
dependency_closure: list[str],
expected: list[str],
) -> None:
"""Component changes narrow the native-ESP8266 test list."""
with (
patch.object(determine_jobs, "changed_files", return_value=changed_files),
patch.object(
determine_jobs,
"get_components_with_dependencies",
return_value=dependency_closure,
),
):
result = determine_jobs.esp8266_native_components_to_test()
assert result == expected
def test_compute_integration_test_buckets_no_durations_full_fanout() -> None:
"""Without recorded durations the fan-out stays at the maximum."""
files = [f"tests/integration/test_{i:03d}.py" for i in range(15)]
@@ -236,3 +236,98 @@ def test_run_grouped_test_closes_group_when_subprocess_raises(
)
assert "::endgroup::" in capsys.readouterr().out
def test_components_empty_match_fails_with_flag(
capsys: pytest.CaptureFixture[str],
) -> None:
"""Under --fail-on-no-tests, a real component filtered to a platform
with no matching test file must not pass CI as a green zero-component
compile."""
rc = tbc.test_components(
["logger"],
"zz-none",
"compile",
False,
enable_grouping=False,
fail_on_no_tests=True,
)
assert rc == 1
assert "No tests ran for requested pattern(s): logger" in (capsys.readouterr().out)
def test_components_component_with_no_base_file_fails_with_flag(
capsys: pytest.CaptureFixture[str],
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A component whose fixture matches the platform but whose platform has
no base file builds nothing; under the flag that silent zero fails by
component name instead of hiding behind other components."""
monkeypatch.setattr(tbc, "get_platform_base_files", lambda base_dir: {})
rc = tbc.test_components(
["logger"],
"esp8266-ard",
"compile",
False,
enable_grouping=False,
fail_on_no_tests=True,
)
assert rc == 1
assert "No tests ran for requested pattern(s): logger" in (capsys.readouterr().out)
def test_components_blank_list_fails_with_flag(
capsys: pytest.CaptureFixture[str],
) -> None:
"""A fully blank component list must not slide into the baseline
fallback and exit green under the flag."""
rc = tbc.test_components(
[""], "esp8266-ard", "compile", False, fail_on_no_tests=True
)
assert rc == 1
assert "blank component list" in capsys.readouterr().out
def test_components_wildcard_no_match_fails_with_flag(
capsys: pytest.CaptureFixture[str],
) -> None:
"""A wildcard matching nothing must not degrade to the synthetic
baseline build and exit green under the flag."""
rc = tbc.test_components(
["zz_no_such*"],
"esp8266-ard",
"compile",
False,
enable_grouping=False,
fail_on_no_tests=True,
)
assert rc == 1
assert "No components found matching" in capsys.readouterr().out
def test_components_empty_match_tolerated_without_flag() -> None:
"""The esp32-ard smoke leg deliberately builds only the subset with a
matching fixture; without the flag an empty match stays green."""
assert (
tbc.test_components(
["logger"], "zz-none", "compile", False, enable_grouping=False
)
== 0
)
def test_components_unknown_component_fails_with_flag(
capsys: pytest.CaptureFixture[str],
) -> None:
"""A renamed smoke-test component must shrink coverage loudly, not fall
into the reference-baseline build."""
rc = tbc.test_components(
["no_such_component_xyz"],
"esp8266-ard",
"compile",
False,
enable_grouping=False,
fail_on_no_tests=True,
)
assert rc == 1
assert "No components found matching" in capsys.readouterr().out
+658 -24
View File
@@ -1,38 +1,34 @@
"""Drift tests for the native ESP8266 Arduino build spec.
"""Drift tests for the native ESP8266 Arduino build generator.
These pin the ESPHome side of the transliteration (the knob-define
precedence, the define/flag sets, and the linker-script generation) against
literals audited from the PlatformIO builder
(framework-arduinoespressif8266/tools/platformio-build.py and
platform-espressif8266/builder/main.py). They catch an accidental edit on
this side; an upstream change in a new framework release is caught by the
A/B byte-identical build check on a version bump, not by these tests.
Pin the transliterated flag/define/link sets against literals audited from
the PlatformIO builder. Upstream drift is caught by the A/B build check on
version bumps, not here.
"""
from __future__ import annotations
from collections.abc import Generator
import logging
import os
from pathlib import Path
import shutil
from unittest.mock import MagicMock, patch
import pytest
from esphome.arduino.library import ArduinoLibrary
from esphome.arduino8266.framework import InstalledPaths, toolchain_tool
from esphome.build_gen import arduino8266
from esphome.build_gen.arduino8266 import (
_defines_flags,
_flag_defines,
_flash_size_str,
_resolve_build_config,
get_flash_ld_path,
)
from esphome.components.esp8266.boards import ESP8266_BOARD_BUILD
from esphome.components.esp8266.boards import BOARDS, ESP8266_BOARD_BUILD
from esphome.components.esp8266.build_surgery import RATETABLE_RULE
from esphome.components.esp8266.const import (
KEY_BOARD,
KEY_ESP8266,
KEY_FLASH_MODE,
KEY_SCANF_FLOAT,
)
from esphome.components.esp8266.const import KEY_BOARD, KEY_ESP8266, KEY_SCANF_FLOAT
import esphome.config_validation as cv
from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION
from esphome.core import CORE, EsphomeError
@@ -47,9 +43,13 @@ def _setup_core(tmp_path: Path) -> Generator[None]:
CORE.data[KEY_CORE] = {KEY_FRAMEWORK_VERSION: cv.Version(3, 1, 2)}
CORE.data[KEY_ESP8266] = {
KEY_BOARD: "nodemcuv2",
KEY_FLASH_MODE: "dout",
KEY_SCANF_FLOAT: False,
}
# The producers esp8266/__init__ pins unconditionally
CORE.platformio_options = {
"board_build.flash_mode": "dout",
"build_src_flags": "-include esphome/components/esp8266/throw_stubs.h",
}
yield
# CORE.reset() (the suite-wide autouse fixture) does not clear this flag
CORE.testing_mode = False
@@ -59,6 +59,11 @@ def _set_flags(*flags: str) -> None:
CORE.build_flags = set(flags)
def _shq(tok: str) -> str:
"""The platform's shell_token quote wrapper (argv rule on Windows)."""
return f'"{tok}"' if os.name == "nt" else f"'{tok}'"
def _resolve(*flags: str) -> arduino8266._BuildConfig:
"""Set the build flags and resolve the knob config in one step."""
_set_flags(*flags)
@@ -206,12 +211,284 @@ def _make_framework(tmp_path: Path) -> InstalledPaths:
for sub in ("include", "ld", "lwip2/include", "lib"):
(framework / "tools" / "sdk" / sub).mkdir(parents=True)
(framework / "libraries").mkdir()
(framework / "tools" / "elf2bin.py").write_text("")
eboot = framework / "bootloaders" / "eboot"
eboot.mkdir(parents=True)
(eboot / "eboot.elf").write_text("")
toolchain = tmp_path / "toolchain"
(toolchain / "bin").mkdir(parents=True)
(toolchain / "include").mkdir()
return InstalledPaths(framework=framework, toolchain=toolchain, ninja=Path("ninja"))
def _write_ninja(
paths: InstalledPaths,
libraries: list | None = None,
ccache: str | None = None,
) -> str:
src = CORE.relative_src_path()
(src / "esphome" / "components" / "esp8266").mkdir(parents=True, exist_ok=True)
(src / "main.cpp").write_text("")
(src / "esphome" / "vendor.c").write_text("")
with (
patch.object(arduino8266, "generate_ld_scripts"),
patch(
"esphome.arduino.library.resolve_libraries",
return_value=libraries or [],
),
):
arduino8266.write_project(paths, ccache)
return (CORE.relative_pioenvs_path(CORE.name) / "build.ninja").read_text()
def test_write_project_rejects_bad_flash_mode(tmp_path: Path) -> None:
"""A flash mode outside the closed set fails by name before landing
unquoted in the elf2bin command."""
paths = _make_framework(tmp_path)
CORE.platformio_options["board_build.flash_mode"] = "dout; rm -rf /"
with pytest.raises(EsphomeError, match="Invalid flash mode"):
_write_ninja(paths)
def test_write_project_flash_mode_reaches_define_and_elf2bin(
tmp_path: Path,
) -> None:
"""A non-default board_flash_mode lands in both the compile define and
the elf2bin image header, never silently falling back to dout."""
paths = _make_framework(tmp_path)
CORE.platformio_options["board_build.flash_mode"] = "dio"
content = _write_ninja(paths)
assert "-DFLASHMODE_DIO" in content
assert "--flash_mode dio" in content
assert "FLASHMODE_DOUT" not in content
def test_write_project_trailing_include_raises(tmp_path: Path) -> None:
"""A dangling -include must fail by name, not become -include <src_dir>."""
paths = _make_framework(tmp_path)
CORE.platformio_options["build_src_flags"] = "-include"
with pytest.raises(EsphomeError, match="trailing '-include'"):
_write_ninja(paths)
def test_write_project_passes_other_src_flags_through(tmp_path: Path) -> None:
"""Non-include build_src_flags tokens are requoted onto the src edges."""
paths = _make_framework(tmp_path)
CORE.platformio_options["build_src_flags"] = (
"-include esphome/components/esp8266/throw_stubs.h -DSRC_ONLY=1"
)
content = _write_ninja(paths)
assert "throw_stubs.h" in content
assert "-DSRC_ONLY=1" in content
def test_write_project_link_line_and_exclusions(tmp_path: Path) -> None:
paths = _make_framework(tmp_path)
_set_flags(
"-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH",
"-DUSE_ESP8266_WAVEFORM_STUBS",
"-Wl,--wrap=millis",
"-Wl,--wrap=printf",
"-Wno-nonnull-compare",
"-L/opt/blobs",
"-luser_blob",
"-L /spc/blobs -l spaced_blob",
)
content = _write_ninja(paths)
# Base link flags from the PlatformIO builder
for flag in (
"-Wl,--no-check-sections",
"-Wl,-static",
"-Wl,--gc-sections",
"-Wl,-wrap,system_restart_local",
"-Wl,-wrap,spi_flash_read",
"-u app_entry",
"-u _printf_float",
"-u _DebugExceptionVector",
"-u _DoubleExceptionVector",
"-u _KernelExceptionVector",
"-u _NMIExceptionVector",
"-u _UserExceptionVector",
):
assert flag in content
# ESPHome's link flags and the board linker script
assert "-Wl,--wrap=millis" in content
assert "-Wl,--wrap=printf" in content
assert "-T eagle.flash.4m.ld" in content
# scanf float disabled: the forced-link flag must not appear
assert "_scanf_float" not in content
# $in/$out must stay unquoted; ninja escapes its own path variables
assert "-c $in -o $out" in content
assert "--app $in --flash_mode" in content
assert '"$in"' not in content
assert '"$out"' not in content
# -L/-l from esphome build_flags reach the link line, not the compiles;
# spaced forms ("-L /path") are shell-lexed the way PlatformIO does.
# str(Path(...)) so the separator matches the host platform.
opt_blobs = str(Path("/opt/blobs"))
spc_blobs = str(Path("/spc/blobs"))
assert f"-L{_shq(opt_blobs)}" in content
assert "-luser_blob" in content
assert f"-L{_shq(spc_blobs)}" in content
assert "-lspaced_blob" in content
for line in content.splitlines():
if line.split(" = ")[0] in ("cflags", "cxxflags", "asflags"):
assert "user_blob" not in line
assert opt_blobs not in line
assert "spaced_blob" not in line
assert spc_blobs not in line
# System libraries with the selected lwIP variant, in the builder's order
assert (
"-lhal -lphy -lpp -lnet80211 -llwip2-1460 -lwpa -lcrypto -lmain -lwps "
"-lbearssl -lespnow -lsmartconfig -lairkiss -lwpa2 -lspaced_blob "
"-luser_blob "
"-lstdc++ -lm -lc -lgcc" in content
)
# Core exclusions: native OTA backend and waveform stubs
assert "Updater.cpp" not in content
assert "core_esp8266_waveform_pwm.cpp" not in content
assert "core_esp8266_waveform_phase.cpp" not in content
assert "core_esp8266_main.cpp.o" in content
# Assembly and C sources compile through their own rules
assert "cont.S.o: aspp" in content
assert "abi.c.o: c" in content
# throw_stubs is force-included for ESPHome sources only, via one shared
# srcflags variable rather than a copy of the flags line per edge
src_lines = [line for line in content.splitlines() if "obj/src/" in line]
assert any("main.cpp.o: cxx" in line for line in src_lines)
assert content.count("throw_stubs.h") == 1
assert "srcflags = -include" in content
flags_lines = [
line for line in content.splitlines() if line.startswith(" flags = ")
]
assert flags_lines
# C++ src edges consume the precompiled header; C/assembly keep srcflags
assert set(flags_lines) == {" flags = $srcflags", " flags = $srccxxflags"}
def test_write_project_pch(tmp_path: Path) -> None:
paths = _make_framework(tmp_path)
_set_flags("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH")
content = _write_ninja(paths, ccache="/usr/bin/ccache")
build_dir = CORE.relative_pioenvs_path(CORE.name)
assert "rule pch" in content
assert "build esphome_pch.h.gch: pch" in content
for line in content.splitlines():
# C++ edges wait on the .gch; the C edge must not reference it
if line.startswith("build obj/src/main.cpp.o:"):
assert line.endswith("| esphome_pch.h.gch")
if line.startswith("build obj/src/esphome/vendor.c.o:"):
assert "esphome_pch" not in line
assert (build_dir / "esphome_pch.h").read_text().splitlines() == [
'#include "esphome/components/esp8266/throw_stubs.h"',
'#include "esphome/core/defines.h"',
]
assert (build_dir / "esphome_pch.h.gch.sum").read_text().strip()
# Emission recorded so the framework env exports the ccache settings
from esphome.build_helpers.pch import ccache_pch_env
assert "CCACHE_PCH_EXTSUM" in ccache_pch_env()
def test_write_project_pch_sum_only_with_ccache(tmp_path: Path) -> None:
"""The .sum sidecar exists solely for ccache; skip it when disabled."""
paths = _make_framework(tmp_path)
_set_flags("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH")
content = _write_ninja(paths)
build_dir = CORE.relative_pioenvs_path(CORE.name)
assert "build esphome_pch.h.gch: pch" in content
assert not (build_dir / "esphome_pch.h.gch.sum").exists()
def test_write_project_pch_identity_unknown_skips_pch(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""An OSError from the checksum means the .sum cannot vouch for the
.gch: the build must fall back to plain srcflags."""
paths = _make_framework(tmp_path)
_set_flags("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH")
with patch(
"esphome.build_gen.arduino8266.pch_checksum",
side_effect=OSError("stat failed"),
):
content = _write_ninja(paths, ccache="/usr/bin/ccache")
assert "esphome_pch" not in content
assert "srccxxflags" not in content
assert " flags = $srcflags" in content
assert "Could not establish the pch identity" in caplog.text
def test_write_project_pch_folds_joined_src_force_include(
tmp_path: Path,
) -> None:
"""-includefoo.h in build_src_flags must fold into the pch like the
separated spelling, not precede and defeat it."""
paths = _make_framework(tmp_path)
_set_flags("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH")
CORE.platformio_options["build_src_flags"] = "-includeesphome/core/defines.h"
content = _write_ninja(paths, ccache="/usr/bin/ccache")
assert "build esphome_pch.h.gch: pch" in content
assert "srccxxflags" in content
assert "-includeesphome" not in content
def test_write_project_pch_skipped_for_joined_force_include_spelling(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""GCC also accepts -includefoo.h as one token; the guard must see it."""
paths = _make_framework(tmp_path)
_set_flags(
"-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH", "-includefoo.h"
)
content = _write_ninja(paths, ccache="/usr/bin/ccache")
assert "esphome_pch" not in content
assert "prevents the precompiled header" in caplog.text
def test_write_project_pch_skipped_when_user_force_include_precedes(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A -include in build_flags lands ahead of the pch include, so GCC
would never load the .gch; skip it and say so."""
paths = _make_framework(tmp_path)
_set_flags(
"-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH", "-include foo.h"
)
content = _write_ninja(paths, ccache="/usr/bin/ccache")
assert "esphome_pch" not in content
assert "srccxxflags" not in content
assert "prevents the precompiled header" in caplog.text
# No pch emitted: the ccache relaxation must stay off
from esphome.build_helpers.pch import ccache_pch_env
assert ccache_pch_env() == {}
def test_write_project_pch_disabled(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("ESPHOME_PCH_ENABLE", "0")
paths = _make_framework(tmp_path)
_set_flags("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH")
content = _write_ninja(paths)
assert "esphome_pch" not in content
assert "srccxxflags" not in content
assert " flags = $srcflags" in content
def test_write_project_scanf_float_and_waveform_kept(tmp_path: Path) -> None:
paths = _make_framework(tmp_path)
CORE.data[KEY_ESP8266][KEY_SCANF_FLOAT] = True
_set_flags("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH")
content = _write_ninja(paths)
assert "-u _scanf_float" in content
# Waveform not stubbed out: both implementations stay in the archive
assert "core_esp8266_waveform_pwm.cpp.o" in content
assert "core_esp8266_waveform_phase.cpp.o" in content
@pytest.mark.parametrize(
("knob", "lib", "mss", "features", "ipv6"),
[
@@ -360,8 +637,8 @@ def test_generate_ld_scripts(tmp_path: Path) -> None:
_run_generate_ld_scripts(paths)
mock_run.assert_not_called()
# A surgery edit invalidates the stamp via the fingerprint (a stale
# linker script would otherwise persist until an esphome clean)
# An edit to the surgery constants invalidates the stamp (a stale linker
# script would otherwise persist until an esphome clean)
with (
patch.object(
arduino8266.build_surgery, "surgery_fingerprint", return_value="changed"
@@ -401,8 +678,6 @@ def test_generate_ld_scripts_testing_mode(tmp_path: Path) -> None:
paths = _make_framework(tmp_path)
(paths.framework / "tools" / "sdk" / "ld" / "eagle.flash.4m.ld").write_text(
# Real flash ld scripts carry no iram1_0_seg (that lives in the
# generated common ld); the surgery rejects one it was not asked about
"MEMORY\n{\n"
" dram0_0_seg : org = 0x3FFE8000, len = 0x14000\n"
" irom0_0_seg : org = 0x40201010, len = 0xfeff0\n"
@@ -416,6 +691,96 @@ def test_generate_ld_scripts_testing_mode(tmp_path: Path) -> None:
assert "len = 0x2000000" in patched
def test_write_project_libraries_and_variant(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
paths = _make_framework(tmp_path)
variant_src = paths.framework / "variants" / "nodemcu" / "variant.cpp"
variant_src.write_text("")
lib_dir = tmp_path / "libsrc"
lib_dir.mkdir()
(lib_dir / "lib.cpp").write_text("")
(lib_dir / "impl.cc").write_text("")
headers_only = ArduinoLibrary(name="HeadersOnly", include_dirs=[lib_dir])
library = ArduinoLibrary(
name="MyLib",
sources=[lib_dir / "impl.cc", lib_dir / "lib.cpp"],
include_dirs=[lib_dir],
flags=["-DMYLIB=1"],
link_dirs=[lib_dir / "blobs"],
link_libs=["algobsec"],
link_flags=["-Wl,--wrap=malloc"],
)
_set_flags("-DPIO_FRAMEWORK_ARDUINO_ENABLE_EXCEPTIONS")
with caplog.at_level(logging.DEBUG, logger="esphome.build_gen.arduino8266"):
content = _write_ninja(
paths, libraries=[library, headers_only], ccache="/cc/ccache"
)
assert "build libFrameworkArduinoVariant.a: ar" in content
assert "build libMyLib.a: ar" in content
# A headers-only library contributes includes but no archive, with a
# debug log distinguishing it from a resolution failure
assert "libHeadersOnly.a" not in content
assert "Library HeadersOnly has no source files" in caplog.text
assert " flags = -DMYLIB=1" in content
assert "-lalgobsec" in content
# Library link flags reach the firmware link line; .cc compiles as C++
assert "-Wl,--wrap=malloc" in content
assert "impl.cc.o: cxx" in content
assert f"-L{_shq(str(lib_dir / 'blobs'))}" in content
# Exceptions knob: -fexceptions and the exception-enabled stdc++
assert "-fexceptions" in content
assert "-lstdc++-exc" in content
assert f"ccache = {_shq('/cc/ccache')}" in content
def test_get_flash_ld_path(tmp_path: Path) -> None:
paths = InstalledPaths(
framework=tmp_path / "framework",
toolchain=tmp_path / "toolchain",
ninja=Path("ninja"),
)
CORE.testing_mode = True
assert get_flash_ld_path(tmp_path, paths) == (
tmp_path / "ld" / "testing_eagle.flash.4m.ld"
)
CORE.testing_mode = False
# Reads the same install the ninja file linked against; no re-resolve
assert get_flash_ld_path(tmp_path, paths) == (
tmp_path / "framework" / "tools" / "sdk" / "ld" / "eagle.flash.4m.ld"
)
def test_flash_size_str() -> None:
assert _flash_size_str(4 * 1024 * 1024) == "4M"
assert _flash_size_str(512 * 1024) == "512K"
def test_write_project_testing_mode(tmp_path: Path) -> None:
paths = _make_framework(tmp_path)
CORE.testing_mode = True
_set_flags()
content = _write_ninja(paths)
assert "-T testing_eagle.flash.4m.ld" in content
assert "ld/testing_eagle.flash.4m.ld" in content
def test_write_project_missing_framework_dir_raises(tmp_path: Path) -> None:
"""An incomplete framework install fails naming the missing path."""
paths = _make_framework(tmp_path)
shutil.rmtree(paths.framework / "tools" / "sdk" / "lwip2")
_set_flags()
with pytest.raises(EsphomeError, match="incomplete.*lwip2"):
_write_ninja(paths)
def test_generate_ld_scripts_testing_mode_missing_flash_ld_raises(
tmp_path: Path,
) -> None:
@@ -440,6 +805,61 @@ def test_build_config_nonosdk_precedence() -> None:
assert _resolve_build_config(_defines()).nonosdk == "NONOSDK221"
def test_write_project_plain_asm_rule_skips_preprocessor(tmp_path: Path) -> None:
"""A lowercase .s source assembles plain (SCons AS), never through the
preprocessor rule that a .S source gets."""
paths = _make_framework(tmp_path)
core_dir = paths.framework / "cores" / "esp8266"
(core_dir / "lowlevel.s").write_text("nop\n")
_set_flags()
content = _write_ninja(paths)
assert "lowlevel.s.o: asm " in content
assert "rule asm\n command = $ccache $cc -x assembler $asflags -c $in -o $out" in (
content
)
def test_write_project_unflags_operandless_linker_flag(tmp_path: Path) -> None:
"""build_unflags: -nostdlib filters whole-token from both lines, as
PlatformIO allows; only operand-taking flags hard-error."""
paths = _make_framework(tmp_path)
_set_flags()
CORE.build_unflags = {"-nostdlib"}
content = _write_ninja(paths)
assert "-nostdlib" not in content
def test_write_project_unflagged_symbol_takes_its_dash_u(tmp_path: Path) -> None:
"""Unflagging a -u symbol drops the -u that carried it; a dangling -u
would consume the next token and hand ld a symbol as an input file."""
paths = _make_framework(tmp_path)
_set_flags()
CORE.build_unflags = {"_printf_float"}
content = _write_ninja(paths)
link_line = next(
line for line in content.splitlines() if line.startswith("linkflags = ")
)
assert "_printf_float" not in link_line
assert "-u -u" not in link_line
# The neighbors survive as intact pairs
assert "-u app_entry" in link_line
assert "-u _DebugExceptionVector" in link_line
def test_write_project_build_unflags_apply_to_framework_flags(tmp_path: Path) -> None:
"""build_unflags removes flags from the framework sets, as PlatformIO does."""
paths = _make_framework(tmp_path)
_set_flags()
CORE.build_unflags = {"-fipa-pta", "-Wl,--gc-sections"}
content = _write_ninja(paths)
for line in content.splitlines():
key = line.split(" = ")[0]
if key in ("cflags", "cxxflags", "asflags"):
assert "-fipa-pta" not in line
if key == "linkflags":
assert "-Wl,--gc-sections" not in line
def test_project_flags_trailing_bare_linker_flag_warns(
caplog: pytest.LogCaptureFixture,
) -> None:
@@ -494,10 +914,18 @@ def test_project_flags_requotes_lexed_defines() -> None:
compile_flags, _link, _dirs, _libs = _split_flags()
# shlex folds the quotes (as PIO's ParseFlags does); _shell_token
# re-quotes the spaced token so the shell passes one argv element
expected = (
'"-DGREETING=hello world"' if os.name == "nt" else "'-DGREETING=hello world'"
)
assert compile_flags == [expected]
assert compile_flags == [_shq("-DGREETING=hello world")]
def test_write_project_empty_core_raises(tmp_path: Path) -> None:
"""A framework tree with no core sources fails at generation, not link."""
paths = _make_framework(tmp_path)
core = paths.framework / "cores" / "esp8266"
for f in core.iterdir():
f.unlink()
_set_flags()
with pytest.raises(EsphomeError, match="no core sources"):
_write_ninja(paths)
def test_flag_defines_joins_spaced_define() -> None:
@@ -508,6 +936,75 @@ def test_flag_defines_joins_spaced_define() -> None:
assert "" not in defines
def test_ninja_path_escaping() -> None:
"""Build-statement paths and command-line paths escape differently."""
assert arduino8266._e("a b:$c") == "a$ b$:$$c"
assert arduino8266._q("/a b/$x") == _shq("/a b/$$x")
def test_write_project_asm_excludes_non_define_user_flags(tmp_path: Path) -> None:
"""The ASPPCOM command under PlatformIO never sees CCFLAGS, so only -D/-I user flags
reach assembly compiles."""
paths = _make_framework(tmp_path)
_set_flags("-DUSER_KNOB=1", "-Wno-volatile")
content = _write_ninja(paths)
asflags = next(line for line in content.splitlines() if line.startswith("asflags"))
assert "-DUSER_KNOB=1" in asflags
assert "-Wno-volatile" not in asflags
cxxflags = next(
line for line in content.splitlines() if line.startswith("cxxflags")
)
assert "-Wno-volatile" in cxxflags
def test_write_project_returns_changed(tmp_path: Path) -> None:
"""The documented contract: True when build.ninja changed, False on an
identical regeneration (pins byte-stable output too)."""
paths = _make_framework(tmp_path)
_set_flags()
src = CORE.relative_src_path()
(src / "esphome" / "components" / "esp8266").mkdir(parents=True, exist_ok=True)
(src / "main.cpp").write_text("")
with (
patch.object(arduino8266, "generate_ld_scripts"),
patch("esphome.arduino.library.resolve_libraries", return_value=[]),
):
assert arduino8266.write_project(paths, None) is True
assert arduino8266.write_project(paths, None) is False
def test_write_project_missing_elf2bin_raises(tmp_path: Path) -> None:
"""A half-extracted package must fail by name at generation, not after
the full compile at the elf2bin edge."""
paths = _make_framework(tmp_path)
(paths.framework / "tools" / "elf2bin.py").unlink()
_set_flags()
src = CORE.relative_src_path()
(src / "main.cpp").parent.mkdir(parents=True, exist_ok=True)
(src / "main.cpp").write_text("")
with (
patch.object(arduino8266, "generate_ld_scripts"),
patch("esphome.arduino.library.resolve_libraries", return_value=[]),
pytest.raises(EsphomeError, match="elf2bin"),
):
arduino8266.write_project(paths, None)
def test_write_project_missing_src_dir_raises(tmp_path: Path) -> None:
"""A missing generated source tree is its own error, not an install one."""
paths = _make_framework(tmp_path)
_set_flags()
with (
patch.object(arduino8266, "generate_ld_scripts"),
patch("esphome.arduino.library.resolve_libraries", return_value=[]),
patch.object(
arduino8266.CORE, "relative_src_path", return_value=tmp_path / "nope"
),
pytest.raises(EsphomeError, match="source directory"),
):
arduino8266.write_project(paths, None)
def test_build_config_custom_mmu_without_knob_raises() -> None:
"""Custom MMU sizes without the CUSTOM knob are refused."""
with pytest.raises(EsphomeError, match="PIO_FRAMEWORK_ARDUINO_MMU_CUSTOM"):
@@ -600,6 +1097,49 @@ def test_generate_ld_scripts_missing_compiler_is_clean(tmp_path: Path) -> None:
_run_generate_ld_scripts(paths)
def test_write_project_asm_keeps_quoted_defines(tmp_path: Path) -> None:
"""A spaced -D/-I user flag arrives shell-quoted; assembly must still
receive it."""
paths = _make_framework(tmp_path)
_set_flags('-DGREETING="hello world"', "-Wno-volatile")
content = _write_ninja(paths)
asflags = next(line for line in content.splitlines() if line.startswith("asflags"))
assert _shq("-DGREETING=hello world") in asflags
assert "-Wno-volatile" not in asflags
def test_write_project_unarchived_library_links_objects(tmp_path: Path) -> None:
"""A libArchive:false library's objects reach the link directly."""
paths = _make_framework(tmp_path)
lib_src = tmp_path / "gdb" / "src"
lib_src.mkdir(parents=True)
(lib_src / "GDBStub.cpp").write_text("")
_set_flags()
lib = ArduinoLibrary(
name="GDBStub",
sources=[lib_src / "GDBStub.cpp"],
include_dirs=[lib_src],
lib_archive=False,
)
content = _write_ninja(paths, libraries=[lib])
assert "libGDBStub.a" not in content
link_line = next(
line for line in content.splitlines() if line.startswith("build firmware.elf")
)
assert "GDBStub.cpp.o" in link_line
def test_write_project_unknown_board_fails_by_name(tmp_path: Path) -> None:
"""A caller bypassing config validation gets the board named, not a
KeyError."""
paths = _make_framework(tmp_path)
_set_flags()
CORE.data[KEY_ESP8266][KEY_BOARD] = "not_a_board"
with pytest.raises(EsphomeError, match="'not_a_board' is not supported"):
_write_ninja(paths)
def test_unflag_tokens_join_spaced_entries() -> None:
"""Spaced build_unflags entries ("-D FOO") match the joined token."""
CORE.build_unflags = {"-D FOO", "-l bar"}
@@ -1072,6 +1612,31 @@ def test_generate_ld_scripts_surgery_failure_is_named(tmp_path: Path) -> None:
_run_generate_ld_scripts(paths)
def test_write_project_unmatched_unflag_warns(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""An unflag that removes nothing is named; a matching one is silent."""
paths = _make_framework(tmp_path)
_set_flags("-DUSE_FOO=1")
CORE.build_unflags = {"-DUSE_FOO", "-Os"}
content = _write_ninja(paths)
assert "matched no build flag: -DUSE_FOO" in caplog.text
assert "-Os" not in caplog.text.split("matched no build flag")[-1].splitlines()[0]
# The matching -Os unflag really removed the framework flag
cflags = next(line for line in content.splitlines() if line.startswith("cflags"))
assert " -Os " not in cflags
def test_write_project_lexes_build_flags_once(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A malformed build_flags entry warns once per generation."""
paths = _make_framework(tmp_path)
_set_flags("-DFOO=1 -l")
_write_ninja(paths)
assert caplog.text.count("Ignoring trailing '-l'") == 1
def test_build_config_mmu_conflict_names_the_variant_knob_with_custom() -> None:
"""With MMU_CUSTOM also set, the actionable fix is dropping the variant
knob, not setting the knob the user already set."""
@@ -1171,6 +1736,11 @@ def test_generate_ld_scripts_unreadable_header_forces_regeneration(
mock_run.assert_called_once()
def test_board_tables_are_equal() -> None:
"""BOARDS and ESP8266_BOARD_BUILD must stay exactly in sync."""
assert set(BOARDS) == set(ESP8266_BOARD_BUILD)
def test_bare_include_and_define_dropped(
caplog: pytest.LogCaptureFixture,
) -> None:
@@ -1208,3 +1778,67 @@ def test_defines_flags_honors_f_cpu_override() -> None:
CORE.platformio_options = {"board_build.f_cpu": "160000000L"}
defines = _defines_flags(config, "dout", "nodemcuv2", board_build["defines"])
assert "-DF_CPU=160000000L" in defines
def test_flash_ld_name_honors_ldscript_override(tmp_path: Path) -> None:
"""board_build.ldscript (filesystem reservation, corrected flash size)
replaces the board default; a path is rejected since the name resolves
via the -L search path."""
assert arduino8266._flash_ld_name("nodemcuv2") == "eagle.flash.4m.ld"
CORE.platformio_options = {"board_build.ldscript": "eagle.flash.4m2m.ld"}
assert arduino8266._flash_ld_name("nodemcuv2") == "eagle.flash.4m2m.ld"
paths = _make_framework(tmp_path)
_set_flags()
content = _write_ninja(paths)
assert "-T eagle.flash.4m2m.ld" in content
CORE.platformio_options = {"board_build.ldscript": "../evil.ld"}
with pytest.raises(EsphomeError, match="bare script name"):
arduino8266._flash_ld_name("nodemcuv2")
def test_unflagging_a_plain_linker_flag_raises(tmp_path: Path) -> None:
"""build_unflags: -u would strip all seven -u tokens and leave the
operands as ld input files; refuse by name instead."""
paths = _make_framework(tmp_path)
_set_flags()
CORE.build_unflags = {"-u _printf_float"}
with pytest.raises(EsphomeError, match="cannot remove plain linker"):
_write_ninja(paths)
def test_d1_wroom_02_keeps_its_shipped_flash_layout() -> None:
"""The board joined BOARDS late; the flash-size default (2m.ld) would
move _FS_end and the preferences sector on existing devices."""
assert arduino8266._flash_ld_name("d1_wroom_02") == "eagle.flash.2m64.ld"
assert arduino8266._flash_ld_name("nodemcuv2") == "eagle.flash.4m.ld"
def test_write_project_rejects_spaced_ldscript_override(tmp_path: Path) -> None:
"""A spaced override never reaches the link line: generate_ld_scripts
rejects the name first (the -T _shell_token quoting behind it is
defence-in-depth)."""
CORE.platformio_options = {"board_build.ldscript": "my script.ld"}
paths = _make_framework(tmp_path)
_set_flags()
with pytest.raises(EsphomeError, match="Invalid flash linker script name"):
arduino8266.write_project(paths, None)
def test_write_project_pch_no_device_path_poison(tmp_path: Path) -> None:
"""Regression: the -include stays relative and the .sum carries no
per-device path, or cross-device ccache sharing breaks."""
paths = _make_framework(tmp_path / "shared")
sums = []
for name in ("dev_a", "dev_b"):
CORE.name = name
CORE.build_path = tmp_path / name
_set_flags("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH")
content = _write_ninja(paths, ccache="/usr/bin/ccache")
assert (
"srccxxflags = -Winvalid-pch -Wno-error=invalid-pch "
"-include esphome_pch.h" in content
)
sums.append(
(CORE.relative_pioenvs_path(name) / "esphome_pch.h.gch.sum").read_text()
)
assert sums[0] == sums[1]
@@ -10,6 +10,7 @@ from unittest.mock import patch
import pytest
from esphome.build_helpers import ccache
from esphome.core import CORE
def test_resolve_opt_out() -> None:
@@ -120,3 +121,17 @@ def test_parse_enable_env_spelling_tables(
"""cv.boolean's spelling tables plus the 1/0 env convention."""
monkeypatch.setenv("ESPHOME_CCACHE_ENABLE", raw)
assert ccache.parse_enable_env("ESPHOME_CCACHE_ENABLE") is expected
def test_effective_ccache_basedir_prefers_user_value(tmp_path: Path) -> None:
CORE.build_path = tmp_path
# Drive-qualified on Windows: "/custom/base" is not absolute there
base = "C:\\custom\\base" if os.name == "nt" else "/custom/base"
with patch.dict(os.environ, {"CCACHE_BASEDIR": base}, clear=True):
assert ccache.effective_ccache_basedir() == base
with patch.dict(os.environ, {}, clear=True):
assert ccache.effective_ccache_basedir() == str(tmp_path.resolve())
# Degenerate values would strip substrings ccache never rewrites
for bad in ("", "/", "a/b"):
with patch.dict(os.environ, {"CCACHE_BASEDIR": bad}, clear=True):
assert ccache.effective_ccache_basedir() == str(tmp_path.resolve())
@@ -85,6 +85,87 @@ def test_parse_entry_resolves_relative_includes() -> None:
assert all(Path(inc).is_absolute() for inc in includes)
def test_parse_entry_resolves_force_include_path(tmp_path: Path) -> None:
"""The pch -include is emitted relative to the build dir; idedata must
resolve it so cached flags work from any cwd."""
(tmp_path / "esphome_pch.h").write_text("")
entry = _entry(
str(tmp_path),
f"{tmp_path}/src/esphome/x.cpp",
"g++ -include esphome_pch.h -c x.cpp",
)
_, _, _, cxx_flags = idedata.parse_entry(entry)
idx = cxx_flags.index("-include")
resolved = cxx_flags[idx + 1]
assert Path(resolved).is_absolute()
assert resolved == str(tmp_path / "esphome_pch.h").replace("\\", "/")
def test_parse_entry_resolves_joined_force_include(tmp_path: Path) -> None:
"""The joined -includefoo.h spelling takes the same resolve path."""
(tmp_path / "esphome_pch.h").write_text("")
entry = _entry(
str(tmp_path),
f"{tmp_path}/src/esphome/x.cpp",
"g++ -includeesphome_pch.h -c x.cpp",
)
_, _, _, cxx_flags = idedata.parse_entry(entry)
resolved = cxx_flags[cxx_flags.index("-include") + 1]
assert resolved == str(tmp_path / "esphome_pch.h").replace("\\", "/")
def test_parse_entry_keeps_search_chain_force_include(tmp_path: Path) -> None:
"""-include names resolved via the -I chain (libretiny's Arduino.h) must
not be re-anchored to a nonexistent build-dir path."""
entry = _entry(
str(tmp_path),
f"{tmp_path}/src/esphome/x.cpp",
"g++ -include Arduino.h -c x.cpp",
)
_, _, _, cxx_flags = idedata.parse_entry(entry)
assert cxx_flags[cxx_flags.index("-include") + 1] == "Arduino.h"
def test_parse_entry_warns_on_vanished_force_include(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A build-dir force-include deleted by clean_build must leave a trail;
a name resolvable via the -I chain must not warn."""
inc = tmp_path / "inc"
inc.mkdir()
(inc / "Arduino.h").write_text("")
entry = _entry(
str(tmp_path),
f"{tmp_path}/src/esphome/x.cpp",
f"g++ -I{inc} -include Arduino.h -include esphome_pch.h -c x.cpp",
)
_, _, _, cxx_flags = idedata.parse_entry(entry)
assert "Arduino.h" in cxx_flags
assert "esphome_pch.h" in caplog.text
assert "Arduino.h" not in caplog.text
def test_parse_entry_drops_trailing_force_include(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
entry = _entry(
str(tmp_path), f"{tmp_path}/src/esphome/x.cpp", "g++ -c x.cpp -include"
)
_, _, _, cxx_flags = idedata.parse_entry(entry)
assert "-include" not in cxx_flags
assert "no argument" in caplog.text
def test_parse_entry_skips_dependency_flags() -> None:
"""Dependency-generation flags (and their args) are dropped."""
entry = _entry(
+218
View File
@@ -0,0 +1,218 @@
"""Tests for esphome.build_helpers.pch."""
from __future__ import annotations
import os
from pathlib import Path
from unittest.mock import patch
import pytest
from esphome.build_helpers import pch
def _write(src_dir: Path, name: str, content: str) -> None:
path = src_dir / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content)
@pytest.mark.parametrize(
("value", "expected"),
[
(None, True),
("1", True),
("0", False),
("false", False),
("", False),
],
)
def test_pch_enabled(value: str | None, expected: bool) -> None:
env = {} if value is None else {"ESPHOME_PCH_ENABLE": value}
with patch.dict(os.environ, env, clear=True):
assert pch.pch_enabled() is expected
def test_ccache_pch_env_empty_until_emitted() -> None:
"""No sloppiness relaxation for a build that skipped the pch."""
with patch.dict(os.environ, {}, clear=True):
assert pch.ccache_pch_env() == {}
def test_ccache_pch_env_enabled() -> None:
pch.mark_pch_emitted()
with patch.dict(os.environ, {}, clear=True):
env = pch.ccache_pch_env()
assert env["CCACHE_SLOPPINESS"] == "pch_defines,time_macros"
assert env["CCACHE_PCH_EXTSUM"] == "true"
def test_ccache_pch_env_disabled() -> None:
with patch.dict(os.environ, {"ESPHOME_PCH_ENABLE": "0"}, clear=True):
assert pch.ccache_pch_env() == {}
def test_ccache_pch_env_token_check_is_membership_not_substring(
caplog: pytest.LogCaptureFixture,
) -> None:
"""A token merely containing ours must not suppress the union."""
pch.mark_pch_emitted()
with patch.dict(os.environ, {"CCACHE_SLOPPINESS": "pch_defines_extra"}, clear=True):
env = pch.ccache_pch_env()
assert env["CCACHE_SLOPPINESS"] == "pch_defines_extra,pch_defines,time_macros"
def test_ccache_pch_env_unions_user_sloppiness(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Without pch_defines/time_macros ccache declines every pch-consuming
compile, so missing tokens are unioned onto the user's value."""
pch.mark_pch_emitted()
with patch.dict(os.environ, {"CCACHE_SLOPPINESS": "locale"}, clear=True):
env = pch.ccache_pch_env()
assert env["CCACHE_SLOPPINESS"] == "locale,pch_defines,time_macros"
assert env["CCACHE_PCH_EXTSUM"] == "true"
assert "Adding pch_defines,time_macros" in caplog.text
caplog.clear()
with patch.dict(
os.environ, {"CCACHE_SLOPPINESS": "pch_defines,time_macros"}, clear=True
):
env = pch.ccache_pch_env()
assert "CCACHE_SLOPPINESS" not in env
assert not caplog.records
def test_pch_header_text_preserves_order() -> None:
text = pch.pch_header_text(["b.h", "a.h"])
assert text == '#include "b.h"\n#include "a.h"\n'
def test_include_closure_resolves_relative_and_root(tmp_path: Path) -> None:
"""Sibling includes resolve against the includer's directory first,
full paths against the src root; unresolvable names end the walk."""
_write(tmp_path, "esphome/components/x/a.h", '#include "b.h"\n')
_write(
tmp_path,
"esphome/components/x/b.h",
'#include "esphome/core/deep.h"\n#include <system.h>\n#include "missing.h"\n',
)
_write(tmp_path, "esphome/core/deep.h", "")
closure = pch._include_closure(tmp_path, ["esphome/components/x/a.h"])
assert sorted(closure) == [
"esphome/components/x/a.h",
"esphome/components/x/b.h",
"esphome/core/deep.h",
]
def test_include_closure_handles_cycles(tmp_path: Path) -> None:
_write(tmp_path, "a.h", '#include "b.h"\n')
_write(tmp_path, "b.h", '#include "a.h"\n')
assert sorted(pch._include_closure(tmp_path, ["a.h"])) == ["a.h", "b.h"]
def test_include_closure_blocks_parent_escape(tmp_path: Path) -> None:
_write(tmp_path / "src", "a.h", '#include "../outside.h"\n')
(tmp_path / "outside.h").write_text("")
assert sorted(pch._include_closure(tmp_path / "src", ["a.h"])) == ["a.h"]
def test_pch_checksum_tracks_closure_content(tmp_path: Path) -> None:
"""A transitive header edit or an extra-identity change must change the
digest; unrelated files must not."""
_write(tmp_path, "root.h", '#include "nested.h"\n')
_write(tmp_path, "nested.h", "int a;\n")
_write(tmp_path, "unrelated.h", "int u;\n")
base = pch.pch_checksum(tmp_path, ["root.h"], ["id"])
assert base == pch.pch_checksum(tmp_path, ["root.h"], ["id"])
assert base != pch.pch_checksum(tmp_path, ["root.h"], ["other-id"])
_write(tmp_path, "unrelated.h", "int changed;\n")
assert base == pch.pch_checksum(tmp_path, ["root.h"], ["id"])
_write(tmp_path, "nested.h", "int b;\n")
assert base != pch.pch_checksum(tmp_path, ["root.h"], ["id"])
@pytest.mark.skipif(
os.name == "nt" or os.geteuid() == 0, reason="chmod is ineffective here"
)
def test_include_closure_fails_closed_on_unreadable(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A marker would truncate the transitive walk; the OSError propagates
so callers compile without a pch."""
_write(tmp_path, "a.h", '#include "locked.h"\n')
locked = tmp_path / "locked.h"
locked.write_text("")
locked.chmod(0)
try:
with pytest.raises(OSError):
pch._include_closure(tmp_path, ["a.h"])
finally:
locked.chmod(0o644)
assert "Could not read locked.h" in caplog.text
def test_pch_extra_scripts_gated(monkeypatch: pytest.MonkeyPatch) -> None:
with patch.dict(os.environ, {}, clear=True):
assert pch.pch_extra_scripts() == ["post:pch.py"]
monkeypatch.setenv("ESPHOME_PCH_ENABLE", "0")
assert pch.pch_extra_scripts() == []
def test_include_closure_raises_when_identity_unknown(
caplog: pytest.LogCaptureFixture,
) -> None:
"""An unreadable header propagates; callers compile without a pch."""
class _BadFile:
def stat(self): # noqa: ANN202 -- regular-file mode only
import os
import stat as stat_mod
return os.stat_result((stat_mod.S_IFREG | 0o644,) + (0,) * 9)
def read_bytes(self) -> bytes:
raise OSError("read failed")
class _FakeSrcDir:
def __truediv__(self, rel: str) -> _BadFile:
return _BadFile()
with pytest.raises(OSError, match="read failed"):
pch._include_closure(_FakeSrcDir(), ["a.h"])
assert "Could not read a.h" in caplog.text
def test_include_closure_survives_non_utf8_include_name(tmp_path: Path) -> None:
"""A non-UTF-8 quoted include must not abort the build; it simply does
not resolve and ends the walk."""
(tmp_path / "a.h").write_bytes(b'#include "bad\xff.h"\n#include "b.h"\n')
(tmp_path / "b.h").write_text("")
closure = pch._include_closure(tmp_path, ["a.h"])
assert set(closure) == {"a.h", "b.h"}
def test_pch_checksum_survives_surrogate_extra(tmp_path: Path) -> None:
"""Install paths from non-UTF-8 filesystems carry surrogates; hashing
them must not raise past the caller's identity-unknown guard."""
assert pch.pch_checksum(tmp_path, [], ["/opt/bad\udcff/framework"])
def test_include_closure_walks_angle_includes_under_src(tmp_path: Path) -> None:
"""An angle include resolving under src/ must enter the digest; one
that does not simply ends the walk."""
_write(tmp_path, "a.h", "#include <local.h>\n#include <Arduino.h>\n")
(tmp_path / "local.h").write_text("")
closure = pch._include_closure(tmp_path, ["a.h"])
assert set(closure) == {"a.h", "local.h"}
def test_ccache_pch_env_warns_on_falsy_extsum(
caplog: pytest.LogCaptureFixture,
) -> None:
"""A user CCACHE_PCH_EXTSUM=false makes ccache hash the .gch bytes."""
pch.mark_pch_emitted()
with patch.dict(os.environ, {"CCACHE_PCH_EXTSUM": "false"}, clear=True):
env = pch.ccache_pch_env()
assert "CCACHE_PCH_EXTSUM" not in env
assert "disables pch caching" in caplog.text
@@ -0,0 +1,193 @@
"""Tests for the native (non-PlatformIO) toolchain config validation."""
from __future__ import annotations
from collections.abc import Generator
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
import pytest
from esphome.components import esp8266
from esphome.components.esp8266 import (
ARDUINO_FRAMEWORK_SCHEMA,
_resolve_toolchain,
_validate_native_toolchain,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_BOARD,
CONF_FRAMEWORK,
CONF_PLATFORM_VERSION,
CONF_SOURCE,
CONF_TOOLCHAIN,
CONF_VERSION,
Toolchain,
)
from esphome.core import CORE, EsphomeError
from esphome.types import ConfigType
@pytest.fixture(autouse=True)
def _arduino_toolchain() -> Generator[None]:
# The suite-wide reset_core fixture clears CORE.toolchain after each test
CORE.toolchain = Toolchain.ARDUINO
esp8266._DECODE_WARNED_AT.clear()
yield
esp8266._DECODE_WARNED_AT.clear()
def _config(
board: str = "nodemcuv2",
version: str = "3.1.2",
source: str | None = None,
platform_version: str | None = None,
) -> ConfigType:
framework: dict[str, str] = {CONF_VERSION: version}
if source is not None:
framework[CONF_SOURCE] = source
if platform_version is not None:
framework[CONF_PLATFORM_VERSION] = platform_version
# The real schema fills the source/platform_version defaults, so these
# tests validate against what config validation actually emits
return {
CONF_FRAMEWORK: ARDUINO_FRAMEWORK_SCHEMA(framework),
CONF_BOARD: board,
}
def test_valid_config_passes() -> None:
config = _config()
assert _validate_native_toolchain(config) is config
def test_platformio_toolchain_skips_checks() -> None:
# 3.0.2 is pio-legal (>= the global 3.0.0 floor) but below the native
# toolchain's own 3.1.1 floor; the bogus board only the native path checks
CORE.toolchain = Toolchain.PLATFORMIO
config = _config(board="not_a_board", version="3.0.2")
assert _validate_native_toolchain(config) is config
def test_version_below_floor_rejected() -> None:
# 3.1.0 has no registry package, so the native floor is 3.1.1
with pytest.raises(cv.Invalid, match="3.1.1 or newer"):
_validate_native_toolchain(_config(version="3.1.0"))
def test_version_at_floor_accepted() -> None:
_validate_native_toolchain(_config(version="3.1.1"))
def test_custom_platform_version_warns_and_is_dropped(
caplog: pytest.LogCaptureFixture,
) -> None:
config = _config(platform_version="platformio/espressif8266@4.0.1")
_validate_native_toolchain(config)
assert "'platform_version' is ignored" in caplog.text
assert CONF_PLATFORM_VERSION not in config[CONF_FRAMEWORK]
def test_default_platform_version_does_not_warn(
caplog: pytest.LogCaptureFixture,
) -> None:
config = _config()
_validate_native_toolchain(config)
assert "'platform_version' is ignored" not in caplog.text
assert CONF_PLATFORM_VERSION not in config[CONF_FRAMEWORK]
def test_custom_source_rejected() -> None:
with pytest.raises(cv.Invalid, match="custom framework source"):
_validate_native_toolchain(
_config(source="https://github.com/esp8266/Arduino.git")
)
def test_unsupported_board_rejected() -> None:
with pytest.raises(cv.Invalid, match="not supported by"):
_validate_native_toolchain(_config(board="not_a_board"))
def test_yaml_toolchain_key_resolves() -> None:
"""The documented `toolchain: arduino` YAML key selects the native path."""
CORE.toolchain = None
_resolve_toolchain({CONF_TOOLCHAIN: Toolchain.ARDUINO})
assert CORE.toolchain == Toolchain.ARDUINO
assert CORE.using_toolchain_arduino
def test_yaml_toolchain_key_defaults_to_platformio() -> None:
CORE.toolchain = None
_resolve_toolchain({})
assert CORE.toolchain == Toolchain.PLATFORMIO
def test_decode_pc_native_missing_tools_warns_once(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A stack dump of many addresses produces one missing-tool warning."""
with (
patch(
"esphome.arduino8266.toolchain.get_addr2line_path",
return_value=tmp_path / "missing-addr2line",
),
patch(
"esphome.arduino8266.toolchain.get_elf_path",
return_value=tmp_path / "missing.elf",
),
):
esp8266._decode_pc({}, "40201234")
esp8266._decode_pc({}, "40201238")
assert caplog.text.count("Cannot decode crash addresses") == 1
def test_decode_pc_platformio_missing_tools_warns_once(
caplog: pytest.LogCaptureFixture,
) -> None:
"""The PlatformIO branch reports a missing addr2line/ELF at the same
warning level as the native one; raw undecoded addresses with no
stated reason are undiagnosable at default log level."""
CORE.toolchain = Toolchain.PLATFORMIO
idedata = SimpleNamespace(addr2line_path=None, firmware_elf_path=None)
with patch("esphome.platformio.toolchain.get_idedata", return_value=idedata):
esp8266._decode_pc({}, "40201234")
esp8266._decode_pc({}, "40201238")
assert caplog.text.count("Cannot decode crash addresses") == 1
def test_resolve_toolchain_rejects_unsupported() -> None:
"""ESP8266 rejects a CLI toolchain it cannot serve, like every platform."""
CORE.toolchain = Toolchain.SDK_NRF
with pytest.raises(cv.Invalid, match="Unsupported toolchain 'sdk-nrf'"):
_resolve_toolchain({})
def test_run_compile_platformio_falls_through() -> None:
"""Under toolchain: platformio the hook returns False without touching
the native backend; this is what keeps existing users on PlatformIO."""
CORE.toolchain = Toolchain.PLATFORMIO
with patch("esphome.arduino8266.toolchain.run_compile") as mock_native:
assert esp8266.run_compile(SimpleNamespace(), {}) is False
mock_native.assert_not_called()
def test_run_compile_arduino_failure_raises() -> None:
"""A non-zero native build fails by name instead of returning success."""
CORE.verbose = False
with (
patch("esphome.arduino8266.toolchain.run_compile", return_value=1),
pytest.raises(EsphomeError, match="native build failed"),
):
esp8266.run_compile(SimpleNamespace(), {})
def test_copy_files_native_skips_platformio_scripts(tmp_path: Path) -> None:
"""The native build writes no PlatformIO extra scripts."""
CORE.build_path = tmp_path
esp8266.copy_files()
assert list(tmp_path.iterdir()) == []
-9
View File
@@ -1429,12 +1429,3 @@ async def test_add_platformio_options_native_arduino(
assert "board_build.ldscript is ignored" in caplog.text
assert "'arduino' toolchain" in caplog.text
assert "upload_speed" not in caplog.text
def test_esp8266_rejects_unsupported_cli_toolchain() -> None:
"""Until the native backend lands, ESP8266 serves only PlatformIO."""
from esphome.components.esp8266 import CONFIG_SCHEMA
CORE.toolchain = Toolchain.ARDUINO
with pytest.raises(cv.Invalid, match="Unsupported toolchain 'arduino'"):
CONFIG_SCHEMA({"board": "nodemcuv2"})
@@ -0,0 +1,65 @@
"""Run the native-toolchain serial-upload path and report heavy modules.
Executed as a subprocess by test_lazy_imports.py: heavy module names come
in on argv, the ones found in sys.modules afterwards go out on stdout.
``upload_using_esptool`` dispatches native toolchains through a
toolchain-keyed table; if someone routes it back through the platform
component packages (esp32 or esp8266), this reports the leak.
"""
import os
from pathlib import Path
import sys
import tempfile
from unittest.mock import patch
from _leak_report import print_leaked_modules
from esphome.__main__ import upload_using_esptool
from esphome.const import (
CONF_ESPHOME,
KEY_CORE,
KEY_ESP32,
KEY_TARGET_PLATFORM,
KEY_VARIANT,
Toolchain,
)
from esphome.core import CORE
# An ambient ESPHOME_USE_SUBPROCESS would route past the patched
# run_external_command into run_external_process and confuse the checks.
os.environ.pop("ESPHOME_USE_SUBPROCESS", None)
config = {CONF_ESPHOME: {"platformio_options": {}}}
with tempfile.TemporaryDirectory() as build_dir:
CORE.name = "leaktest"
CORE.build_path = build_dir
for platform, toolchain, backend in (
("esp8266", Toolchain.ARDUINO, "esphome.arduino8266.toolchain"),
("esp32", Toolchain.ESP_IDF, "esphome.espidf.toolchain"),
):
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: platform}
if platform == "esp32":
CORE.data[KEY_ESP32] = {KEY_VARIANT: "ESP32S3"}
CORE.toolchain = toolchain
import importlib
image = importlib.import_module(backend).get_factory_firmware_path()
image.parent.mkdir(parents=True, exist_ok=True)
image.write_bytes(b"\x00")
with patch("esphome.__main__.run_external_command", return_value=0) as mock_run:
rc = upload_using_esptool(config, "/dev/ttyUSB0", None, None)
# Fail loudly if the upload path stopped doing its work; otherwise
# an empty leak list could just mean nothing ran.
if rc != 0:
sys.exit(f"upload_using_esptool({platform}) returned {rc}")
cmd = list(mock_run.call_args[0][1:])
if str(image) not in [str(Path(c)) for c in cmd]:
sys.exit(f"native factory image did not reach esptool: {cmd}")
print_leaked_modules()
@@ -0,0 +1,657 @@
"""Tests for esphome.arduino8266.toolchain (the ninja build driver)."""
from __future__ import annotations
import os
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from esphome.arduino8266 import framework, toolchain
from esphome.build_helpers.pch import mark_pch_emitted
import esphome.config_validation as cv
from esphome.const import (
CONF_COMPILE_PROCESS_LIMIT,
CONF_ESPHOME,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
)
from esphome.core import CORE, EsphomeError
_SIZE_OUTPUT = """\
firmware.elf :
section size addr
.data 1924 1073643520
.noinit 56 1073645444
.text 496 1074790400
.irom0.text 342804 1075843088
.text1 27489 1074790896
.rodata 2588 1073645504
.bss 26504 1073648096
Total 401861
"""
@pytest.fixture(autouse=True)
def _setup_core(tmp_path: Path) -> None:
CORE.name = "test8266"
CORE.config_path = tmp_path / "test8266.yaml"
CORE.build_path = tmp_path
CORE.data[KEY_CORE] = {KEY_FRAMEWORK_VERSION: cv.Version(3, 1, 2)}
# run_compile verifies the produced artifacts; give every test a build
# that "produced" them (tests for the guard delete them again). The
# manifest comes first: artifacts must not be older than build.ninja.
build_dir = CORE.relative_pioenvs_path("test8266")
build_dir.mkdir(parents=True, exist_ok=True)
(build_dir / "build.ninja").write_text("# manifest")
for artifact in (
"firmware.elf",
"firmware.bin",
"firmware.factory.bin",
"firmware.ota.bin",
):
(build_dir / artifact).write_bytes(b"")
def _paths(tmp_path: Path) -> framework.InstalledPaths:
return framework.InstalledPaths(
framework=tmp_path / "framework",
toolchain=tmp_path / "toolchain",
ninja=tmp_path / "ninja",
)
def test_path_getters(tmp_path: Path) -> None:
assert toolchain.get_build_dir() == CORE.relative_pioenvs_path("test8266")
assert toolchain.get_elf_path().name == "firmware.elf"
# The framework accessor owns the layout and the Windows suffix
suffix = ".exe" if os.name == "nt" else ""
assert toolchain.get_addr2line_path().name == f"xtensa-lx106-elf-addr2line{suffix}"
assert toolchain.get_objdump_path().name == f"xtensa-lx106-elf-objdump{suffix}"
assert toolchain.get_readelf_path().name == f"xtensa-lx106-elf-readelf{suffix}"
def test_run_compile_build_failure(tmp_path: Path) -> None:
with (
patch.object(framework, "check_and_install", return_value=_paths(tmp_path)),
patch.object(framework, "get_build_env", return_value={}),
patch("esphome.build_gen.arduino8266.write_project"),
patch.object(
toolchain.subprocess, "run", return_value=MagicMock(returncode=2)
) as mock_run,
patch.object(toolchain, "_write_compile_commands") as mock_compdb,
):
assert toolchain.run_compile({CONF_ESPHOME: {}}, verbose=True) == 2
cmd = mock_run.call_args[0][0]
assert "-v" in cmd
# The compile database is generated before the build runs, so a failed
# build cannot leave a stale database behind.
mock_compdb.assert_called_once()
def test_run_compile_success(tmp_path: Path) -> None:
with (
patch.object(framework, "check_and_install", return_value=_paths(tmp_path)),
patch.object(framework, "get_build_env", return_value={}),
# An unchanged manifest is what makes the -n probe run
patch("esphome.build_gen.arduino8266.write_project", return_value=False),
patch.object(
toolchain.subprocess,
"run",
return_value=MagicMock(returncode=0, stdout="", stderr=""),
) as mock_run,
patch.object(toolchain, "_write_compile_commands") as mock_compdb,
patch.object(toolchain, "_print_size_summary") as mock_size,
patch.object(toolchain, "get_idedata") as mock_idedata,
):
rc = toolchain.run_compile(
{CONF_ESPHOME: {CONF_COMPILE_PROCESS_LIMIT: 4}}, verbose=False
)
assert rc == 0
# The -n probe runs first, then the real build (cwd, no -C banner)
ninja_calls = [c for c in mock_run.call_args_list if "ninja" in str(c[0][0][0])]
assert "-n" in ninja_calls[0][0][0]
# Explicit targets: a manifest missing them fails as "unknown target"
assert ninja_calls[0][0][0][-1] == "firmware.ota.bin"
cmd = ninja_calls[1][0][0]
assert cmd[-4:] == ["-j", "4", "firmware.factory.bin", "firmware.ota.bin"]
assert "-C" not in cmd
assert ninja_calls[1][1]["cwd"] is not None
mock_compdb.assert_called_once()
mock_size.assert_called_once()
mock_idedata.assert_called_once()
def test_run_compile_noop_skips_the_build_spawn(tmp_path: Path) -> None:
"""A no-op rebuild stays quiet: the -n probe answers "no work to do"
and the real ninja spawn (and its banner) never happens."""
with (
patch.object(framework, "check_and_install", return_value=_paths(tmp_path)),
patch.object(framework, "get_build_env", return_value={}),
# An unchanged manifest is what makes the -n probe run
patch("esphome.build_gen.arduino8266.write_project", return_value=False),
patch.object(
toolchain.subprocess,
"run",
return_value=MagicMock(
returncode=0, stdout="ninja: no work to do.\n", stderr=""
),
) as mock_run,
patch.object(toolchain, "_write_compile_commands"),
patch.object(toolchain, "_print_size_summary"),
patch.object(toolchain, "get_idedata"),
):
rc = toolchain.run_compile({CONF_ESPHOME: {}}, verbose=False)
assert rc == 0
ninja_calls = [c for c in mock_run.call_args_list if "ninja" in str(c[0][0][0])]
assert len(ninja_calls) == 1
assert "-n" in ninja_calls[0][0][0]
# Explicit targets: a manifest missing them fails as "unknown target"
assert ninja_calls[0][0][0][-1] == "firmware.ota.bin"
def test_run_compile_regenerates_stale_compdb(tmp_path: Path) -> None:
"""An interrupted run can leave build.ninja newer than the compile DB;
mere existence must not skip regeneration."""
build_dir = toolchain.get_build_dir()
build_dir.mkdir(parents=True, exist_ok=True)
(build_dir / "build.ninja").write_text("")
compdb = build_dir / "compile_commands.json"
compdb.write_text("[]")
os.utime(compdb, ((build_dir / "build.ninja").stat().st_mtime - 5,) * 2)
with (
patch.object(framework, "check_and_install", return_value=_paths(tmp_path)),
patch.object(framework, "get_build_env", return_value={}),
patch("esphome.build_gen.arduino8266.write_project", return_value=False),
patch.object(
toolchain.subprocess,
"run",
return_value=MagicMock(
returncode=0, stdout="ninja: no work to do.\n", stderr=""
),
),
patch.object(toolchain, "_write_compile_commands") as mock_compdb,
patch.object(toolchain, "_print_size_summary"),
patch.object(toolchain, "get_idedata"),
):
assert toolchain.run_compile({CONF_ESPHOME: {}}, verbose=False) == 0
mock_compdb.assert_called_once()
def test_run_compile_surfaces_probe_diagnostics(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A load-time ninja diagnostic (a generator bug signal) reaches the
user even when the no-work branch skips the real spawn."""
with (
patch.object(framework, "check_and_install", return_value=_paths(tmp_path)),
patch.object(framework, "get_build_env", return_value={}),
# An unchanged manifest is what makes the -n probe run
patch("esphome.build_gen.arduino8266.write_project", return_value=False),
patch.object(
toolchain.subprocess,
"run",
return_value=MagicMock(
returncode=0,
stdout="ninja: no work to do.\n",
stderr="ninja: warning: multiple rules generate x\n",
),
),
patch.object(toolchain, "_write_compile_commands"),
patch.object(toolchain, "_print_size_summary"),
patch.object(toolchain, "get_idedata"),
):
rc = toolchain.run_compile({CONF_ESPHOME: {}}, verbose=False)
assert rc == 0
assert "multiple rules generate x" in caplog.text
def test_run_compile_missing_artifact_fails(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A zero ninja exit that produced no firmware must not be a green
build (size summary and idedata only warn)."""
(toolchain.get_build_dir() / "firmware.elf").unlink()
with (
patch.object(framework, "check_and_install", return_value=_paths(tmp_path)),
patch.object(framework, "get_build_env", return_value={}),
patch("esphome.build_gen.arduino8266.write_project"),
patch.object(
toolchain.subprocess,
"run",
return_value=MagicMock(returncode=0, stdout="", stderr=""),
),
patch.object(toolchain, "_write_compile_commands"),
patch.object(toolchain, "_print_size_summary") as mock_size,
patch.object(toolchain, "get_idedata"),
):
rc = toolchain.run_compile({CONF_ESPHOME: {}}, verbose=False)
assert rc == 1
assert "Build produced no" in caplog.text
mock_size.assert_not_called()
def test_run_compile_warns_when_idedata_fails(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A failed idedata generation right after a successful build is visible,
not deferred to a misleading error in a later command."""
with (
patch.object(framework, "check_and_install", return_value=_paths(tmp_path)),
patch.object(framework, "get_build_env", return_value={}),
patch("esphome.build_gen.arduino8266.write_project"),
patch.object(
toolchain.subprocess,
"run",
return_value=MagicMock(returncode=0, stdout="", stderr=""),
),
patch.object(toolchain, "_write_compile_commands"),
patch.object(toolchain, "_print_size_summary"),
patch.object(toolchain, "get_idedata", return_value=None),
):
assert toolchain.run_compile({CONF_ESPHOME: {}}, verbose=False) == 0
assert "No idedata was generated for this build" in caplog.text
def test_write_compile_commands(tmp_path: Path) -> None:
build_dir = tmp_path / "build"
build_dir.mkdir()
entries = '[{"file": "a.cpp", "command": "cc"}]\n'
with patch.object(
toolchain.subprocess,
"run",
return_value=MagicMock(returncode=0, stdout=entries),
):
toolchain._write_compile_commands(tmp_path / "ninja", build_dir, {})
assert (build_dir / "compile_commands.json").read_text() == entries
@pytest.mark.parametrize(
("stdout", "match"),
[
("[]\n", "empty compile database"),
# A parse failure names its cause, not the rule-name story
("not json", "unparsable compile database.*not json"),
],
)
def test_write_compile_commands_bad_db_raises(
tmp_path: Path, stdout: str, match: str
) -> None:
"""An empty or unparsable compile database fails the build with its
actual cause and drops any stale database."""
build_dir = tmp_path / "build"
build_dir.mkdir()
(build_dir / "compile_commands.json").write_text("[stale]")
with (
patch.object(
toolchain.subprocess,
"run",
return_value=MagicMock(returncode=0, stdout=stdout),
),
pytest.raises(EsphomeError, match=match),
):
toolchain._write_compile_commands(tmp_path / "ninja", build_dir, {})
assert not (build_dir / "compile_commands.json").exists()
def test_write_compile_commands_failure_removes_stale_db(tmp_path: Path) -> None:
"""A failed compdb run must not leave a stale database behind."""
stale = tmp_path / "compile_commands.json"
stale.write_text("[]")
with (
patch.object(
toolchain.subprocess,
"run",
return_value=MagicMock(returncode=1, stderr="boom"),
),
pytest.raises(EsphomeError, match="compile_commands"),
):
toolchain._write_compile_commands(tmp_path / "ninja", tmp_path, {})
assert not stale.exists()
def test_parse_app_size(tmp_path: Path) -> None:
ld = tmp_path / "eagle.flash.4m.ld"
ld.write_text("MEMORY\n{\n irom0_0_seg : org = 0x40201010, len = 0xfeff0\n}\n")
with patch("esphome.build_gen.arduino8266.get_flash_ld_path", return_value=ld):
assert toolchain._parse_app_size(tmp_path, _paths(tmp_path)) == 0xFEFF0
ld.write_text("MEMORY { }\n")
with patch("esphome.build_gen.arduino8266.get_flash_ld_path", return_value=ld):
assert toolchain._parse_app_size(tmp_path, _paths(tmp_path)) is None
# A zero-length segment is bad data, not a budget; warn and drop it
ld.write_text("MEMORY\n{\n irom0_0_seg : org = 0x40201010, len = 0x0\n}\n")
with patch("esphome.build_gen.arduino8266.get_flash_ld_path", return_value=ld):
assert toolchain._parse_app_size(tmp_path, _paths(tmp_path)) is None
with patch(
"esphome.build_gen.arduino8266.get_flash_ld_path",
return_value=tmp_path / "missing.ld",
):
assert toolchain._parse_app_size(tmp_path, _paths(tmp_path)) is None
def test_print_size_summary(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
with (
patch.object(
toolchain.subprocess,
"run",
return_value=MagicMock(returncode=0, stdout=_SIZE_OUTPUT),
),
patch.object(toolchain, "_parse_app_size", return_value=1044464),
):
toolchain._print_size_summary(tmp_path, _paths(tmp_path))
out = capsys.readouterr().out
# Exact PlatformIO shape so script/ci_memory_impact_extract.py can parse it
assert "RAM: [==== ] 37.9% (used 31016 bytes from 81920 bytes)" in out
assert "Flash: [==== ] 35.9% (used 375301 bytes from 1044464 bytes)" in out
def test_print_size_summary_missing_size_tool_warns(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A missing size binary degrades to a warning; the firmware already
linked and must not be discarded."""
with patch.object(
toolchain.subprocess, "run", side_effect=FileNotFoundError("no size")
):
toolchain._print_size_summary(tmp_path, _paths(tmp_path))
assert "Could not summarize firmware size" in caplog.text
def test_print_size_summary_no_app_size(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
with (
patch.object(
toolchain.subprocess,
"run",
return_value=MagicMock(returncode=0, stdout=_SIZE_OUTPUT),
),
patch.object(toolchain, "_parse_app_size", return_value=None),
):
toolchain._print_size_summary(tmp_path, _paths(tmp_path))
out = capsys.readouterr().out
# Both lines are skipped together: a RAM line without Flash would skew
# CI's memory-impact sums across builds
assert out == ""
def test_print_size_summary_size_tool_failure(
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
caplog: pytest.LogCaptureFixture,
) -> None:
with patch.object(
toolchain.subprocess,
"run",
return_value=MagicMock(returncode=1, stdout="", stderr="bad elf"),
):
toolchain._print_size_summary(tmp_path, _paths(tmp_path))
assert capsys.readouterr().out == ""
assert "Could not summarize firmware size" in caplog.text
def test_get_idedata_delegates(tmp_path: Path) -> None:
with (
patch(
"esphome.build_helpers.idedata.load_or_build_idedata",
return_value={"cc_path": "x"},
) as mock_load,
patch.object(toolchain, "resolve_ccache_path", return_value="/cc/ccache"),
):
assert toolchain.get_idedata() == {"cc_path": "x"}
compile_commands, elf, cache = mock_load.call_args[0]
assert compile_commands.name == "compile_commands.json"
assert elf.name == "firmware.elf"
assert cache.name == "test8266.arduino.json"
# The exact configured launcher string is passed for compile DB parsing
# (resolve_ccache_path returns a str, untouched on every platform)
assert mock_load.call_args.kwargs["launcher"] == "/cc/ccache"
def test_get_idedata_no_ccache(tmp_path: Path) -> None:
with (
patch(
"esphome.build_helpers.idedata.load_or_build_idedata", return_value={}
) as mock_load,
patch.object(toolchain, "resolve_ccache_path", return_value=None),
):
toolchain.get_idedata()
assert mock_load.call_args.kwargs["launcher"] is None
def test_run_compile_skips_compdb_when_ninja_unchanged(tmp_path: Path) -> None:
"""An unchanged build.ninja means the compile DB is already current."""
build_dir = toolchain.get_build_dir()
build_dir.mkdir(parents=True, exist_ok=True)
# write_project (stubbed below) always leaves a build.ninja behind
(build_dir / "build.ninja").write_text("# manifest")
def run(regenerate_expected: bool) -> None:
with (
patch.object(framework, "check_and_install", return_value=_paths(tmp_path)),
patch.object(framework, "get_build_env", return_value={}),
patch("esphome.build_gen.arduino8266.write_project", return_value=False),
patch.object(
toolchain.subprocess,
"run",
return_value=MagicMock(returncode=0, stdout="", stderr=""),
),
patch.object(toolchain, "_write_compile_commands") as mock_compdb,
patch.object(toolchain, "_print_size_summary"),
patch.object(toolchain, "get_idedata"),
):
assert toolchain.run_compile({CONF_ESPHOME: {}}, verbose=False) == 0
assert mock_compdb.called == regenerate_expected
# Missing compile DB: regenerated even though build.ninja is unchanged
run(regenerate_expected=True)
# Present compile DB + unchanged build.ninja: skipped
(build_dir / "compile_commands.json").write_text("[]")
run(regenerate_expected=False)
def test_print_size_summary_unparsable_section(
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
caplog: pytest.LogCaptureFixture,
) -> None:
"""A totals-relevant section that fails to parse must not produce a
confident wrong number; an irrelevant one only warns."""
bad = _SIZE_OUTPUT.replace(".bss 26504", ".bss abc")
with patch.object(
toolchain.subprocess,
"run",
return_value=MagicMock(returncode=0, stdout=bad),
):
toolchain._print_size_summary(tmp_path, _paths(tmp_path))
assert capsys.readouterr().out == ""
assert "Unparsable size output" in caplog.text
caplog.clear()
harmless = _SIZE_OUTPUT + ".broken abc 0\n"
with (
patch.object(
toolchain.subprocess,
"run",
return_value=MagicMock(returncode=0, stdout=harmless),
),
patch.object(toolchain, "_parse_app_size", return_value=1044464),
):
toolchain._print_size_summary(tmp_path, _paths(tmp_path))
assert "RAM:" in capsys.readouterr().out
assert "Unparsable size output" in caplog.text
def test_print_size_summary_missing_section_skips_summary(
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
caplog: pytest.LogCaptureFixture,
) -> None:
"""A totals section absent from the output must not default to zero."""
without_bss = "\n".join(
line for line in _SIZE_OUTPUT.splitlines() if ".bss" not in line
)
with patch.object(
toolchain.subprocess,
"run",
return_value=MagicMock(returncode=0, stdout=without_bss),
):
toolchain._print_size_summary(tmp_path, _paths(tmp_path))
assert capsys.readouterr().out == ""
assert "missing section(s) .bss" in caplog.text
def test_warn_ignored_platformio_options(caplog: pytest.LogCaptureFixture) -> None:
"""Component-added options the native build drops are warned by name;
the honored ones (lib_ignore, f_cpu, ldscript, build_src_flags,
flash_mode) stay quiet."""
CORE.platformio_options = {
"board_build.ldscript": "eagle.flash.4m2m.ld",
"board_build.f_cpu": "160000000L",
"board_build.filesystem": "littlefs",
"board_build.flash_mode": "dio",
"build_src_flags": "-include throw_stubs.h",
"lib_ignore": ["Updater"],
"upload_speed": "460800",
}
toolchain._warn_ignored_platformio_options()
assert "platformio_options->board_build.filesystem is ignored" in caplog.text
assert "native 'arduino' toolchain" in caplog.text
assert "board_build.ldscript is ignored" not in caplog.text
assert "board_build.f_cpu is ignored" not in caplog.text
assert "lib_ignore" not in caplog.text
assert "build_src_flags" not in caplog.text
assert "flash_mode" not in caplog.text
# Component-added upload_speed never gets read under the native
# toolchain, so it must warn
assert "upload_speed" in caplog.text
def test_run_compile_idedata_error_does_not_fail_build(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""An unusable compile DB after a successful build warns, never fails."""
with (
patch.object(framework, "check_and_install", return_value=_paths(tmp_path)),
patch.object(framework, "get_build_env", return_value={}),
patch("esphome.build_gen.arduino8266.write_project"),
patch.object(
toolchain.subprocess,
"run",
return_value=MagicMock(returncode=0, stdout="", stderr=""),
),
patch.object(toolchain, "_write_compile_commands"),
patch.object(toolchain, "_print_size_summary"),
patch.object(
toolchain,
"get_idedata",
side_effect=EsphomeError("compile database is unusable"),
),
):
assert toolchain.run_compile({CONF_ESPHOME: {}}, verbose=False) == 0
assert "Could not generate idedata: compile database is unusable" in caplog.text
def test_run_compile_skipped_size_summary_names_consequence(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A skipped RAM/Flash summary warns about the missing metric; the
per-cause warnings alone are invisible to CI harnesses."""
with (
patch.object(framework, "check_and_install", return_value=_paths(tmp_path)),
patch.object(framework, "get_build_env", return_value={}),
patch("esphome.build_gen.arduino8266.write_project"),
patch.object(
toolchain.subprocess,
"run",
return_value=MagicMock(returncode=0, stdout="", stderr=""),
),
patch.object(toolchain, "_write_compile_commands"),
patch.object(toolchain, "_print_size_summary", return_value=False),
patch.object(toolchain, "get_idedata", return_value=None),
):
assert toolchain.run_compile({CONF_ESPHOME: {}}, verbose=False) == 0
assert "Firmware size summary unavailable for this build" in caplog.text
def test_parse_app_size_non_utf8_ld_warns(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A corrupt (non-UTF-8) linker script degrades to the same warning as
an unreadable one, never a traceback after a successful link."""
paths = _paths(tmp_path)
ld = tmp_path / "corrupt.ld"
ld.write_bytes(b"\xff\xfe not utf8")
with patch("esphome.build_gen.arduino8266.get_flash_ld_path", return_value=ld):
assert toolchain._parse_app_size(tmp_path, paths) is None
assert "Cannot read linker script" in caplog.text
def test_run_compile_failed_probe_runs_full_build(tmp_path: Path) -> None:
"""A failing -n probe (e.g. unknown target from a defective manifest)
falls through to the real build so the error prints attributably."""
probe = MagicMock(returncode=1, stdout="", stderr="")
ok = MagicMock(returncode=0, stdout="", stderr="")
def fake_run(cmd, *args, **kwargs):
return probe if "-n" in cmd else ok
with (
patch.object(framework, "check_and_install", return_value=_paths(tmp_path)),
patch.object(framework, "get_build_env", return_value={}),
patch("esphome.build_gen.arduino8266.write_project", return_value=False),
patch.object(toolchain.subprocess, "run", side_effect=fake_run) as mock_run,
patch.object(toolchain, "_write_compile_commands"),
patch.object(toolchain, "_print_size_summary", return_value=True),
patch.object(toolchain, "get_idedata", return_value={}),
):
assert toolchain.run_compile({CONF_ESPHOME: {}}, verbose=False) == 0
# The real build ran after the failed probe
assert any(
"firmware.ota.bin" in c[0][0] and "-n" not in c[0][0]
for c in mock_run.call_args_list
)
def test_get_idedata_accepts_preresolved_ccache() -> None:
"""run_compile threads its resolved ccache through; the probe must not
run again."""
with (
patch(
"esphome.build_helpers.idedata.load_or_build_idedata",
return_value={"ok": True},
) as mock_build,
patch.object(toolchain, "resolve_ccache_path") as mock_resolve,
):
assert toolchain.get_idedata("/usr/bin/ccache") == {"ok": True}
mock_resolve.assert_not_called()
assert mock_build.call_args.kwargs["launcher"] == "/usr/bin/ccache"
def test_ccache_env_includes_pch_settings() -> None:
"""The native build exports the ccache settings the pch needs."""
mark_pch_emitted()
with patch.dict(os.environ, {}, clear=True):
env = framework.ccache_env("/usr/bin/ccache")
assert env["CCACHE_SLOPPINESS"] == "pch_defines,time_macros"
assert env["CCACHE_PCH_EXTSUM"] == "true"
def test_ccache_env_pch_disabled() -> None:
with patch.dict(os.environ, {"ESPHOME_PCH_ENABLE": "0"}, clear=True):
env = framework.ccache_env("/usr/bin/ccache")
assert "CCACHE_SLOPPINESS" not in env
assert "CCACHE_PCH_EXTSUM" not in env
def test_ccache_env_respects_user_sloppiness() -> None:
mark_pch_emitted()
with patch.dict(os.environ, {"CCACHE_SLOPPINESS": "locale"}, clear=True):
env = framework.ccache_env("/usr/bin/ccache")
# The user's tokens survive; the ones the pch needs are unioned on
assert env["CCACHE_SLOPPINESS"] == "locale,pch_defines,time_macros"
assert env["CCACHE_PCH_EXTSUM"] == "true"
+21
View File
@@ -175,6 +175,27 @@ def test_esptool_upload_fast_path_does_not_import_heavy_modules(
)
def test_native_upload_fast_path_does_not_import_heavy_modules(
fixture_path: Path,
probe_env: dict[str, str],
) -> None:
"""The native-toolchain serial upload dispatches through a
toolchain-keyed backend table; resolving the flash image must not drag
in the esp32 or esp8266 component package or the validation stack.
"""
leaked = _leaked_from_fixture(
fixture_path,
probe_env,
"native_upload_fast_path.py",
extra=("esphome.components.esp8266",),
)
assert not leaked, (
f"native upload_using_esptool pulls in heavy modules: {leaked}. "
"The upload fast path skips validation; importing a platform "
"component package executes its codegen module by design."
)
def test_api_client_does_not_import_heavy_modules() -> None:
"""``esphome.api_client`` is on the logs fast path and must stay light.
+262 -3
View File
@@ -13,7 +13,7 @@ import sys
import time
from types import SimpleNamespace
from typing import Any, Self
from unittest.mock import AsyncMock, MagicMock, Mock, patch
from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, patch
import pytest
from pytest import CaptureFixture
@@ -25,6 +25,7 @@ from esphome.__main__ import (
Purpose,
_get_configured_xtal_freq,
_make_crystal_freq_callback,
_native_toolchain_module,
_redact_with_legacy_fallback,
_resolve_network_devices,
_should_subscribe_states,
@@ -7030,7 +7031,7 @@ def test_command_run_rp2040_bootsel_redetects_serial_port() -> None:
def test_command_idedata_esp_idf_prints_json(capsys: CaptureFixture) -> None:
"""Under the native ESP-IDF toolchain, idedata is emitted as JSON."""
setup_core()
setup_core(platform=PLATFORM_ESP32)
CORE.toolchain = Toolchain.ESP_IDF
data = {"cxx_path": "g++", "prog_path": "/build/firmware.elf"}
@@ -7044,7 +7045,7 @@ def test_command_idedata_esp_idf_prints_json(capsys: CaptureFixture) -> None:
def test_command_idedata_esp_idf_no_build_errors() -> None:
"""Under ESP-IDF, a missing build (no idedata) returns an error, not a crash."""
setup_core()
setup_core(platform=PLATFORM_ESP32)
CORE.toolchain = Toolchain.ESP_IDF
with patch("esphome.espidf.toolchain.get_idedata", return_value=None):
@@ -7218,6 +7219,201 @@ def test_warn_source_tree_mismatch_falls_back_when_stat_fails(
assert not caplog.text
def test_upload_using_esptool_arduino_toolchain(
tmp_path: Path,
mock_run_external_command_main: Mock,
) -> None:
"""The native ESP8266 Arduino toolchain flashes its factory image at
0x0, resolved from the toolchain-keyed backend table (deliberately not
the platform hook: that import would break the upload fast path)."""
setup_core(platform=PLATFORM_ESP8266, tmp_path=tmp_path, name="test")
CORE.toolchain = Toolchain.ARDUINO
from esphome.arduino8266 import toolchain as native
factory = native.get_factory_firmware_path()
factory.parent.mkdir(parents=True, exist_ok=True)
factory.touch()
config = {CONF_ESPHOME: {"platformio_options": {}}}
result = upload_using_esptool(config, "/dev/ttyUSB0", None, None)
assert result == 0
cmd_list = list(mock_run_external_command_main.call_args[0][1:])
firmware_offset_idx = cmd_list.index("write-flash") + 4
assert cmd_list[firmware_offset_idx] == "0x0"
assert cmd_list[firmware_offset_idx + 1] == str(factory)
@pytest.mark.parametrize(
("toolchain", "pio_project_written"),
[
# The native toolchain generates its project at compile time, so
# write_cpp_file must not write a platformio.ini; the default
# toolchain writes the PlatformIO project files.
(Toolchain.ARDUINO, False),
(None, True),
],
)
def test_write_cpp_file_project_generation_follows_toolchain(
tmp_path: Path, toolchain: Toolchain | None, pio_project_written: bool
) -> None:
setup_core(platform=PLATFORM_ESP8266, tmp_path=tmp_path, name="test")
CORE.toolchain = toolchain
with (
patch("esphome.writer.write_cpp") as mock_write_cpp,
patch("esphome.build_gen.platformio.write_project") as mock_pio_project,
patch.object(
type(CORE), "cpp_main_section", new_callable=PropertyMock
) as mock_section,
):
mock_section.return_value = ""
assert main.write_cpp_file() == 0
mock_write_cpp.assert_called_once()
assert mock_pio_project.called is pio_project_written
def test_command_idedata_arduino_prints_json(
tmp_path: Path, capsys: CaptureFixture
) -> None:
"""Under the native ESP8266 Arduino toolchain, idedata is emitted as JSON."""
setup_core(platform=PLATFORM_ESP8266, tmp_path=tmp_path)
CORE.toolchain = Toolchain.ARDUINO
data = {"cxx_path": "g++", "prog_path": "/build/firmware.elf"}
with patch(
"esphome.arduino8266.toolchain.get_idedata", return_value=data
) as mock_get:
result = command_idedata(MagicMock(), CORE.config)
assert result == 0
mock_get.assert_called_once_with()
assert json.loads(capsys.readouterr().out) == data
def test_command_idedata_arduino_no_build_errors(tmp_path: Path) -> None:
"""A missing native build (no idedata) returns an error, not a crash."""
setup_core(platform=PLATFORM_ESP8266, tmp_path=tmp_path)
CORE.toolchain = Toolchain.ARDUINO
with patch("esphome.arduino8266.toolchain.get_idedata", return_value=None):
result = command_idedata(MagicMock(), CORE.config)
assert result == 1
@pytest.mark.parametrize(
("platform", "toolchain", "module"),
[
(PLATFORM_ESP8266, Toolchain.ARDUINO, "esphome.arduino8266.toolchain"),
(PLATFORM_ESP32, Toolchain.ESP_IDF, "esphome.espidf.toolchain"),
],
)
def test_command_analyze_memory_native_toolchains(
tmp_path: Path,
mock_write_cpp: Mock,
mock_compile_program: Mock,
mock_get_esphome_components: Mock,
mock_memory_analyzer_cli: Mock,
mock_ram_strings_analyzer: Mock,
platform: str,
toolchain: Toolchain,
module: str,
) -> None:
"""analyze-memory uses the native toolchain's binutils instead of
falling into the PlatformIO branch."""
setup_core(platform=platform, tmp_path=tmp_path, name="test_device")
CORE.toolchain = toolchain
config = {CONF_ESPHOME: {CONF_NAME: "test_device"}}
# The tools must exist: a missing binutils now fails by name instead of
# silently falling back to host tools
objdump = tmp_path / "objdump"
readelf = tmp_path / "readelf"
objdump.write_text("")
readelf.write_text("")
# The ELF must exist too: the analyzer swallows tool failures, so a
# missing image would report zeroes with exit 0
firmware_elf = tmp_path / "firmware.elf"
firmware_elf.write_text("")
with (
patch(f"{module}.get_objdump_path", return_value=objdump),
patch(f"{module}.get_readelf_path", return_value=readelf),
patch(f"{module}.get_elf_path", return_value=firmware_elf),
):
result = command_analyze_memory(MockArgs(), config)
assert result == 0
mock_memory_analyzer_cli.assert_called_once_with(
str(firmware_elf),
str(objdump),
str(readelf),
set(),
idedata=None,
)
def test_command_analyze_memory_native_missing_elf_fails(
tmp_path: Path,
mock_write_cpp: Mock,
mock_compile_program: Mock,
mock_get_esphome_components: Mock,
mock_memory_analyzer_cli: Mock,
mock_ram_strings_analyzer: Mock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""A missing firmware.elf fails by name instead of an exit-0 zeroed
report."""
setup_core(platform=PLATFORM_ESP8266, tmp_path=tmp_path, name="test_device")
CORE.toolchain = Toolchain.ARDUINO
config = {CONF_ESPHOME: {CONF_NAME: "test_device"}}
objdump = tmp_path / "objdump"
readelf = tmp_path / "readelf"
objdump.write_text("")
readelf.write_text("")
module = "esphome.arduino8266.toolchain"
with (
patch(f"{module}.get_objdump_path", return_value=objdump),
patch(f"{module}.get_readelf_path", return_value=readelf),
patch(f"{module}.get_elf_path", return_value=tmp_path / "missing.elf"),
):
result = command_analyze_memory(MockArgs(), config)
assert result == 1
assert "compile the configuration first" in caplog.text
mock_memory_analyzer_cli.assert_not_called()
def test_command_analyze_memory_missing_binutils_fails_by_name(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A truncated toolchain install fails naming the missing tool instead
of silently analyzing with host binutils."""
setup_core(platform="esp8266", tmp_path=tmp_path, name="test_device")
CORE.toolchain = Toolchain.ARDUINO
config = {CONF_ESPHOME: {CONF_NAME: "test_device"}}
module = "esphome.arduino8266.toolchain"
with (
patch(f"{module}.get_objdump_path", return_value=tmp_path / "missing-objdump"),
patch(f"{module}.get_readelf_path", return_value=tmp_path / "readelf"),
patch("esphome.__main__.write_cpp", return_value=0),
patch("esphome.__main__.compile_program", return_value=0),
):
assert command_analyze_memory(MockArgs(), config) == 1
assert "missing-objdump" in caplog.text
assert "toolchain install may be incomplete" in caplog.text
def test_command_idedata_incompatible_toolchain(tmp_path: Path) -> None:
"""A non-native, non-platformio toolchain errors out cleanly."""
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path)
CORE.toolchain = Toolchain.SDK_NRF
assert command_idedata(MagicMock(), CORE.config) == 1
@pytest.mark.parametrize(
"error",
[
@@ -7294,6 +7490,46 @@ def test_compile_program_espidf_idedata_none_warns(
assert "No idedata was generated" in caplog.text
def test_native_toolchain_table_serves_every_native_toolchain() -> None:
"""Every member of NATIVE_TOOLCHAINS has a backend entry; a gap would
surface as a targeted EsphomeError on the one affected config, and this
pin keeps the table from drifting when a toolchain is added."""
from esphome.const import NATIVE_TOOLCHAINS
assert {tc for _, tc in main._NATIVE_TOOLCHAIN_MODULES} == set(NATIVE_TOOLCHAINS)
def test_native_toolchain_module_missing_backend_raises(tmp_path: Path) -> None:
"""A native toolchain missing from the backend table is a bug and must
fail, not silently degrade to the PlatformIO path."""
setup_core(platform=PLATFORM_ESP8266, tmp_path=tmp_path, name="test_device")
CORE.toolchain = Toolchain.ARDUINO
with (
patch.dict(main._NATIVE_TOOLCHAIN_MODULES, clear=True),
pytest.raises(EsphomeError, match="no native build backend"),
):
_native_toolchain_module()
def test_command_analyze_memory_unsupported_toolchain(
tmp_path: Path,
mock_write_cpp: Mock,
mock_compile_program: Mock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""A hook-less non-PlatformIO toolchain is refused by name, never routed
into the PlatformIO branch."""
setup_core(platform=PLATFORM_NRF52, tmp_path=tmp_path, name="test_device")
CORE.toolchain = Toolchain.SDK_NRF
mock_write_cpp.return_value = 0
mock_compile_program.return_value = 0
result = command_analyze_memory(MockArgs(), {CONF_ESPHOME: {CONF_NAME: "t"}})
assert result == 1
assert "analyze-memory is not supported" in caplog.text
def test_cli_toolchain_skips_the_validated_config_cache(tmp_path: Path) -> None:
"""An explicit --toolchain must run the per-platform validators, so the
upload/logs fast path becomes a cache miss."""
@@ -7309,6 +7545,29 @@ def test_cli_toolchain_skips_the_validated_config_cache(tmp_path: Path) -> None:
mock_read.assert_called_once()
def test_upload_using_esptool_native_missing_firmware_raises(
tmp_path: Path,
) -> None:
"""A stale or absent firmware.bin fails by name instead of flashing air."""
setup_core(platform=PLATFORM_ESP8266, tmp_path=tmp_path, name="test")
CORE.toolchain = Toolchain.ARDUINO
with pytest.raises(EsphomeError, match="compile the configuration first"):
upload_using_esptool(
{CONF_ESPHOME: {"platformio_options": {}}}, "/dev/ttyUSB0", None, None
)
def test_compile_program_unclaimed_native_toolchain_raises(
tmp_path: Path,
) -> None:
"""A resolved native toolchain no platform backend claims must fail,
never fall through to the PlatformIO project path."""
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test_device")
CORE.toolchain = Toolchain.ARDUINO # esp32 has no arduino-native backend
with pytest.raises(EsphomeError, match="no platform backend claimed"):
compile_program(MockArgs(), {})
def test_cli_toolchain_still_refreshes_the_validated_config_cache(
tmp_path: Path,
) -> None:
@@ -0,0 +1,460 @@
"""Tests for esphome/platformio/pch.py.script against a fake SCons env."""
from __future__ import annotations
import os
from pathlib import Path
import stat
from unittest.mock import patch
import pytest
from esphome.platformio import toolchain
pytestmark = pytest.mark.skipif(
os.name == "nt", reason="the fake compiler is a POSIX shell script"
)
_SCRIPT = Path(toolchain.__file__).parent / "pch.py.script"
class _FakePlatform:
packages = {"framework-x": {}, "toolchain-y": {}}
def get_package_version(self, name: str) -> str:
if name == "toolchain-y":
raise KeyError(name)
return "1.2.3"
def get_package(self, name: str) -> object | None:
return None
class _BrokenPlatform(_FakePlatform):
def get_package_version(self, name: str) -> str:
raise RuntimeError("manifest parse error")
class _UnresolvedPlatform(_FakePlatform):
"""KeyError from a package that IS installed: unresolved identity."""
def get_package_version(self, name: str) -> str:
raise KeyError(name)
def get_package(self, name: str) -> object:
return object()
class _FakeSConsEnv(dict):
"""Just enough of a SCons construction environment for pch.py."""
def __init__(
self,
proj_dir: Path,
src_dir: Path,
cxx: str,
flags: list[str],
platform_cls: type[_FakePlatform] = _FakePlatform,
):
super().__init__(ENV={})
self._subst = {
"$PROJECT_DIR": str(proj_dir),
"$PROJECT_SRC_DIR": str(src_dir),
"$CXX": cxx,
}
self._flags = flags
self._platform_cls = platform_cls
self.prepended: list[str] = []
def subst(self, expr: str) -> str: # noqa: N802
return self._subst[expr]
def subst_list(self, expr: str) -> list[list[str]]: # noqa: N802
return [self._flags]
def PioPlatform(self) -> _FakePlatform: # noqa: N802
return self._platform_cls()
def Prepend(self, CXXFLAGS: list[str]) -> None: # noqa: N802, N803
self.prepended = CXXFLAGS
def _fake_cxx(
tmp_path: Path,
fail: bool = False,
fail_msg: str | None = None,
reject_pch: bool = False,
probe_exit: int = 0,
) -> Path:
"""A compiler stand-in that records its argv and writes the -o target.
With reject_pch it builds the .gch fine but, like GCC 10 on macOS arm64,
warns on any consuming compile that the .gch cannot be loaded; probe_exit
sets the exit code of non-header compiles (the load probe).
"""
cxx = tmp_path / "fake-gxx"
body = (
'printf -- ---call---\\\\n >> "$0.argv"; printf \'%s\\n\' "$@" >> "$0.argv"\n'
)
if fail:
body += f"echo {fail_msg or 'boom'} >&2\nexit 1\n"
else:
# Only the c++-header compile has a -o; the load probe has none
body += 'out=""; prev=""; mf=0; dep=0\nfor a in "$@"; do [ "$prev" = "-o" ] && out="$a"; prev="$a"; [ "$a" = "-MF" ] && mf=1; case "$a" in -M|-MM|-MD|-MMD) dep=1;; esac; done\n'
# Real cc1plus rejects -MF without a dependency flag
body += 'if [ "$mf" = 1 ] && [ "$dep" = 0 ]; then echo "cc1plus: error: to generate dependencies you must specify either \x27-M\x27 or \x27-MM\x27" >&2; exit 1; fi\n'
body += '[ -n "$out" ] && echo gch > "$out"\n'
if reject_pch:
body += 'case " $* " in *c++-header*) ;; *) echo "warning: esphome_pch.h.gch: had text segment at different address" >&2;; esac\n'
body += f'case " $* " in *c++-header*) exit 0;; *) exit {probe_exit};; esac\n'
cxx.write_text("#!/bin/sh\n" + body)
cxx.chmod(cxx.stat().st_mode | stat.S_IEXEC)
return cxx
def _run_script(
tmp_path: Path,
flags: list[str] | None = None,
fail: bool = False,
fail_msg: str | None = None,
reject_pch: bool = False,
probe_exit: int = 0,
missing_cxx: bool = False,
env_vars: dict[str, str] | None = None,
name: str = "dev",
platform_cls: type[_FakePlatform] = _FakePlatform,
) -> _FakeSConsEnv:
proj = tmp_path / name
src = proj / "src"
(src / "esphome" / "core").mkdir(parents=True, exist_ok=True)
(src / "esphome" / "core" / "defines.h").write_text("#define USE_X\n")
cxx = _fake_cxx(
tmp_path,
fail=fail,
fail_msg=fail_msg,
reject_pch=reject_pch,
probe_exit=probe_exit,
)
if missing_cxx:
cxx = tmp_path / "no-such-gxx"
args = (proj, src, str(cxx), flags or ["-DX=1"], platform_cls)
# Distinct objects: the -include flags must land on projenv only
global_env = _FakeSConsEnv(*args)
projenv = _FakeSConsEnv(*args)
projenv.global_env = global_env
source = _SCRIPT.read_text()
with patch.dict(os.environ, env_vars or {}, clear=True):
exec( # noqa: S102
compile(source, "pch.py", "exec"),
{"Import": lambda *_names: None, "env": global_env, "projenv": projenv},
)
return projenv
def test_pch_script_builds_and_prepends_relative_include(tmp_path: Path) -> None:
scons_env = _run_script(tmp_path)
proj = tmp_path / "dev"
assert (proj / "esphome_pch.h").read_text().endswith('"esphome/core/defines.h"\n')
assert (proj / "esphome_pch.h.gch").is_file()
assert len((proj / "esphome_pch.h.gch.sum").read_text().strip()) == 64
# Relative include: an absolute path would poison ccache keys
assert scons_env.prepended == [
"-Winvalid-pch",
"-Wno-error=invalid-pch",
"-include",
"esphome_pch.h",
]
# In production projenv["ENV"] aliases os.environ; only the -include
# flags are genuinely scoped to projenv (src compiles)
assert scons_env["ENV"]["CCACHE_SLOPPINESS"] == "pch_defines,time_macros"
assert scons_env["ENV"]["CCACHE_PCH_EXTSUM"] == "true"
assert scons_env.global_env.prepended == []
def test_pch_script_preserves_spaced_flag_elements(tmp_path: Path) -> None:
"""One SCons element stays one compiler argv; -include pairs are
stripped from the .gch compile."""
spaced = tmp_path / "My Configs"
spaced.mkdir()
(tmp_path / "dev" / "src").mkdir(parents=True, exist_ok=True)
(tmp_path / "dev" / "src" / "other.h").write_text("")
flags = ['-DUSB_PRODUCT=\\"Pico 2W\\"', "-I", str(spaced), "-include", "other.h"]
_run_script(tmp_path, flags=flags)
calls = (tmp_path / "fake-gxx.argv").read_text().split("---call---\n")
gch_call = next(c for c in calls if "c++-header" in c).splitlines()
assert '-DUSB_PRODUCT="Pico 2W"' in gch_call
assert str(spaced) in gch_call
assert "-include" not in gch_call
# The stripped src-resolvable -include is folded into the prefix header
pch = (tmp_path / "dev" / "esphome_pch.h").read_text()
assert pch.splitlines()[0] == '#include "other.h"'
def test_pch_script_folds_joined_force_include_spelling(tmp_path: Path) -> None:
"""-includefoo.h folds like the separated form, matching the native path."""
(tmp_path / "dev" / "src").mkdir(parents=True, exist_ok=True)
(tmp_path / "dev" / "src" / "other.h").write_text("")
_run_script(tmp_path, flags=["-DX=1", "-includeother.h"])
pch = (tmp_path / "dev" / "esphome_pch.h").read_text()
assert pch.splitlines()[0] == '#include "other.h"'
def test_pch_script_leaves_absolute_force_includes_unfolded(
tmp_path: Path,
) -> None:
"""An absolute -include resolves through src_dir / name; it must still
stay consumer-only or the host path enters the .sum."""
outside = tmp_path / "outside.h"
outside.write_text("")
_run_script(tmp_path, flags=["-DX=1", "-include", str(outside)])
pch = (tmp_path / "dev" / "esphome_pch.h").read_text()
assert "outside.h" not in pch
def test_pch_script_leaves_non_src_force_includes_unfolded(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""A user -include outside src/ must not enter the prefix header:
consumers keep their own copy, so folding an unguarded header would
include it twice."""
_run_script(tmp_path, flags=["-DX=1", "-include", "user_extra.h"])
pch = (tmp_path / "dev" / "esphome_pch.h").read_text()
assert "user_extra.h" not in pch
assert pch.splitlines()[-1] == '#include "esphome/core/defines.h"'
assert "not precompiling non-src force-includes" in capsys.readouterr().out
def test_pch_script_sum_is_device_independent(tmp_path: Path) -> None:
"""Regression: identical configs in different dirs share cache keys."""
sums = []
for name in ("dev_a", "dev_b"):
proj = tmp_path / name
_run_script(
tmp_path,
flags=["-DX=1", "-I", str(proj / "include")],
env_vars={"CCACHE_BASEDIR": str(proj)},
name=name,
)
sums.append((proj / "esphome_pch.h.gch.sum").read_text())
(tmp_path / "fake-gxx").unlink()
(tmp_path / "fake-gxx.argv").unlink(missing_ok=True)
assert sums[0] == sums[1]
def test_pch_script_transient_compiler_failure_does_not_latch(
tmp_path: Path,
) -> None:
"""ENOSPC-style failures clear on their own; no .failed marker."""
scons_env = _run_script(tmp_path, fail=True, fail_msg="No space left on device")
proj = tmp_path / "dev"
assert not (proj / "esphome_pch.h.gch.failed").exists()
assert scons_env.prepended == []
def test_pch_script_failure_marker_suppresses_retry(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
scons_env = _run_script(tmp_path, fail=True)
proj = tmp_path / "dev"
assert (proj / "esphome_pch.h.gch.failed").is_file()
assert not (proj / "esphome_pch.h.gch.sum").exists()
assert scons_env.prepended == []
# Second run: same checksum, no compile attempt, but says so
attempts = (tmp_path / "fake-gxx.argv").read_text().count("c++-header")
_run_script(tmp_path, fail=True)
out = capsys.readouterr().out
assert (tmp_path / "fake-gxx.argv").read_text().count("c++-header") == attempts
assert "delete esphome_pch.h.gch.failed to retry" in out
def test_pch_script_probe_rejection_falls_back(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""A toolchain that cannot load its own .gch (GCC 10 on macOS arm64)
must not leave consumers paying for a pch every compile rejects."""
scons_env = _run_script(tmp_path, reject_pch=True)
proj = tmp_path / "dev"
assert not (proj / "esphome_pch.h.gch").exists()
assert not (proj / "esphome_pch.h.gch.sum").exists()
assert (proj / "esphome_pch.h.gch.failed").is_file()
assert scons_env.prepended == []
assert "toolchain cannot load the pch" in capsys.readouterr().out
def test_pch_script_spawn_failure_is_transient(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""A spawn failure must not latch a .failed marker (matches espidf)."""
scons_env = _run_script(tmp_path, missing_cxx=True)
proj = tmp_path / "dev"
assert not (proj / "esphome_pch.h.gch.failed").exists()
assert not (proj / "esphome_pch.h.gch.sum").exists()
assert scons_env.prepended == []
assert "did not run" in capsys.readouterr().out
def test_pch_script_probe_nonzero_exit_falls_back(tmp_path: Path) -> None:
"""A probe failure whose stderr never mentions .gch must still count."""
scons_env = _run_script(tmp_path, probe_exit=1)
proj = tmp_path / "dev"
assert not (proj / "esphome_pch.h.gch").exists()
assert (proj / "esphome_pch.h.gch.failed").is_file()
assert scons_env.prepended == []
def test_pch_script_unresolved_package_version_skips_pch(tmp_path: Path) -> None:
"""A KeyError for an installed package is unresolved identity, not absence."""
scons_env = _run_script(tmp_path, platform_cls=_UnresolvedPlatform)
assert not (tmp_path / "dev" / "esphome_pch.h.gch").exists()
assert scons_env.prepended == []
def test_pch_script_package_version_error_skips_pch(tmp_path: Path) -> None:
"""Without trustworthy package identity a stale .gch could survive an
upgrade, so the script must not build one at all."""
scons_env = _run_script(tmp_path, platform_cls=_BrokenPlatform)
proj = tmp_path / "dev"
assert not (proj / "esphome_pch.h.gch").exists()
assert scons_env.prepended == []
def test_pch_script_corrupt_sidecar_reads_as_stale(tmp_path: Path) -> None:
"""A truncated/corrupt .failed marker must not disable the pch forever."""
_run_script(tmp_path, fail=True)
proj = tmp_path / "dev"
(proj / "esphome_pch.h.gch.failed").write_bytes(b"\xff\xfe corrupt")
_run_script(tmp_path)
assert (proj / "esphome_pch.h.gch").is_file()
assert (proj / "esphome_pch.h.gch.sum").is_file()
def test_pch_script_rebuilds_when_header_missing(tmp_path: Path) -> None:
_run_script(tmp_path)
proj = tmp_path / "dev"
(proj / "esphome_pch.h").unlink()
_run_script(tmp_path)
assert (proj / "esphome_pch.h").is_file()
def test_copy_pch_script(tmp_path: Path) -> None:
from esphome.core import CORE
CORE.build_path = tmp_path
toolchain.copy_pch_script()
assert (tmp_path / "pch.py").read_text() == _SCRIPT.read_text()
def test_pch_script_unions_user_sloppiness(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""A user CCACHE_SLOPPINESS without the pch tokens gets them unioned on,
mirroring ccache_pch_env, or every src TU is a permanent miss."""
scons_env = _run_script(tmp_path, env_vars={"CCACHE_SLOPPINESS": "locale"})
assert scons_env["ENV"]["CCACHE_SLOPPINESS"] == "locale,pch_defines,time_macros"
assert "adding pch_defines,time_macros" in capsys.readouterr().out
def test_pch_script_unmodelable_flag_skips_pch(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""Unbalanced quotes and multi-token elements cannot be reproduced as
one argv; the pch is skipped rather than built with diverging flags."""
for bad in ("-DFOO='bar", "-DA=1\t-DB=2"):
scons_env = _run_script(tmp_path, flags=["-DX=1", bad])
assert scons_env.prepended == []
assert not (tmp_path / "dev" / "esphome_pch.h.gch").exists()
assert "unmodelable flag" in capsys.readouterr().out
@pytest.mark.skipif(
getattr(os, "geteuid", lambda: -1)() == 0, reason="root ignores file modes"
)
def test_pch_script_unlistable_include_dir_skips_pch(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""An unlistable subtree must not silently drop out of the digest."""
proj = tmp_path / "dev"
override = proj / "lwip_override"
hidden = override / "hidden"
hidden.mkdir(parents=True)
(hidden / "gen.h").write_text("")
hidden.chmod(0)
try:
scons_env = _run_script(tmp_path, flags=["-DX=1", "-I", str(override)])
finally:
hidden.chmod(0o755)
assert scons_env.prepended == []
assert "skipping precompiled header" in capsys.readouterr().out
def test_pch_script_nobuild_without_projenv_is_noop(tmp_path: Path) -> None:
"""-t nobuild never exports projenv; the script must not abort."""
proj = tmp_path / "dev"
(proj / "src").mkdir(parents=True)
def strict_import(*names: str) -> None:
if "projenv" in names:
raise RuntimeError("Import of non-existent variable 'projenv'")
env = _FakeSConsEnv(proj, proj / "src", "g++", ["-DX=1"])
exec( # noqa: S102
compile(_SCRIPT.read_text(), "pch.py", "exec"),
{"Import": strict_import, "env": env},
)
assert not (proj / "esphome_pch.h").exists()
def test_pch_script_ignores_library_trees_and_non_headers(tmp_path: Path) -> None:
""".piolibdeps and non-header files must not enter the digest (or be
read at all); package versions already cover library identity."""
proj = tmp_path / "dev"
libdeps = proj / ".piolibdeps" / "lib" / "src"
libdeps.mkdir(parents=True)
(libdeps / "lib.h").write_text("#define A 1\n")
override = proj / "lwip_override"
override.mkdir(parents=True)
(override / "lwipopts.h").write_text("#define TCP_MSS 1460\n")
(override / "notes.txt").write_text("v1\n")
flags = ["-DX=1", "-I", str(libdeps), "-I", str(override)]
_run_script(tmp_path, flags=flags)
first = (proj / "esphome_pch.h.gch.sum").read_text()
(libdeps / "lib.h").write_text("#define A 2\n")
(override / "notes.txt").write_text("v2\n")
(tmp_path / "fake-gxx.argv").unlink(missing_ok=True)
_run_script(tmp_path, flags=flags)
assert (proj / "esphome_pch.h.gch.sum").read_text() == first
def test_pch_script_hashes_project_local_include_dirs(tmp_path: Path) -> None:
"""Generated headers in project-local -I dirs (e.g. rp2's lwip_override)
must invalidate the checksum when they change."""
proj = tmp_path / "dev"
override = proj / "lwip_override"
override.mkdir(parents=True)
(override / "lwipopts.h").write_text("#define TCP_MSS 1460\n")
flags = ["-DX=1", "-I", str(override)]
_run_script(tmp_path, flags=flags)
first = (proj / "esphome_pch.h.gch.sum").read_text()
(override / "lwipopts.h").write_text("#define TCP_MSS 536\n")
(tmp_path / "fake-gxx.argv").unlink(missing_ok=True)
_run_script(tmp_path, flags=flags)
assert (proj / "esphome_pch.h.gch.sum").read_text() != first
@pytest.mark.skipif(
getattr(os, "geteuid", lambda: -1)() == 0, reason="root ignores file modes"
)
def test_pch_script_unreadable_local_header_skips_pch(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""An unreadable generated header means unknown identity: no pch."""
proj = tmp_path / "dev"
override = proj / "lwip_override"
override.mkdir(parents=True)
secret = override / "lwipopts.h"
secret.write_text("#define TCP_MSS 1460\n")
secret.chmod(0)
scons_env = _run_script(tmp_path, flags=["-DX=1", "-I", str(override)])
assert not (proj / "esphome_pch.h.gch.sum").exists()
assert scons_env.prepended == []
assert "skipping precompiled header" in capsys.readouterr().out
+31 -1
View File
@@ -516,10 +516,13 @@ def test_clean_build(
dependencies_lock = tmp_path / "dependencies.lock"
dependencies_lock.write_text("lock file")
# idedata cache lives under the data dir, not the build path.
# idedata caches live under the data dir, not the build path; the
# .arduino.json variant is the native esp8266 toolchain's.
idedata_cache = tmp_path / "idedata" / "test.json"
idedata_cache.parent.mkdir()
idedata_cache.write_text("{}")
arduino_idedata_cache = tmp_path / "idedata" / "test.arduino.json"
arduino_idedata_cache.write_text("{}")
# Native ESP-IDF toolchain artifacts.
idf_build_dir = tmp_path / "build"
@@ -580,6 +583,7 @@ def test_clean_build(
assert not piolibdeps_dir.exists()
assert not dependencies_lock.exists()
assert not idedata_cache.exists()
assert not arduino_idedata_cache.exists()
assert not idf_build_dir.exists()
assert not managed_components_dir.exists()
assert not pio_components_dir.exists()
@@ -673,6 +677,32 @@ def test_clean_build_partial_exists(
assert "dependencies.lock" not in caplog.text
@patch("esphome.writer.CORE")
def test_clean_build_partial_removes_pch_artifacts(
mock_core: MagicMock,
tmp_path: Path,
) -> None:
"""The PlatformIO pch sidecars live at the project root and must go in
a partial clean, like the native backend's under .pioenvs."""
names = (
"esphome_pch.h",
"esphome_pch.h.gch",
"esphome_pch.h.gch.sum",
"esphome_pch.h.gch.failed",
)
for name in names:
(tmp_path / name).write_text("x")
mock_core.relative_pioenvs_path.return_value = tmp_path / ".pioenvs"
mock_core.relative_piolibdeps_path.return_value = tmp_path / ".piolibdeps"
mock_core.relative_build_path.side_effect = lambda name: tmp_path / name
mock_core.relative_internal_path.side_effect = tmp_path.joinpath
clean_build()
for name in names:
assert not (tmp_path / name).exists()
@patch("esphome.writer.CORE")
def test_clean_build_nothing_exists(
mock_core: MagicMock,