diff --git a/esphome/build_gen/arduino8266.py b/esphome/build_gen/arduino8266.py index e33512681d..f26869b6cc 100644 --- a/esphome/build_gen/arduino8266.py +++ b/esphome/build_gen/arduino8266.py @@ -17,14 +17,18 @@ from __future__ import annotations from dataclasses import dataclass, field import logging from pathlib import Path -import re import subprocess +from typing import TYPE_CHECKING +from esphome.build_helpers.ninja import shell_token as _shell_token from esphome.components.esp8266 import build_surgery from esphome.core import CORE, EsphomeError from esphome.helpers import mkdir_p, write_file_if_changed from esphome.platformio.library import join_flag_args, split_flag_entry +if TYPE_CHECKING: + from esphome.arduino8266.framework import InstalledPaths + _LOGGER = logging.getLogger(__name__) # From platformio-build.py. The first entry is the default; with multiple SDK @@ -234,38 +238,6 @@ def _resolve_build_config(defines: dict[str, str]) -> _BuildConfig: ) -def _quote_arg(tok: str) -> str: - """Wrap a token in double quotes with the Windows argv rule. - - Same escaping rule as ``subprocess.list2cmdline``: a backslash run - doubles only immediately before a quote (or the closing quote), and the - quote itself is escaped. POSIX sh parses the result identically for - backslashes and quotes. ``$`` must already be doubled for ninja. - """ - quoted = re.sub(r'(\\*)"', lambda m: m.group(1) * 2 + '\\"', tok) - quoted = re.sub(r"(\\+)\Z", lambda m: m.group(1) * 2, quoted) - return f'"{quoted}"' - - -_NEEDS_QUOTE = re.compile(r'[\s"\']') - - -def _shell_token(tok: str) -> str: - """Quote a lexed token only when needed; ``_q`` force-quotes paths. - - Lexing strips the quoting a user wrote (``-DX="a b"`` becomes the single - token ``-DX=a b``); re-quote on the way out so the compiler receives the - same argv element SCons would pass under PlatformIO. After ninja - un-doubles ``$$``, sh still expands ``$VAR`` while CreateProcess passes - it literally -- the same divergence SCons-under-sh has, so this stays - PlatformIO parity. - """ - tok = tok.replace("$", "$$") # ninja would expand a bare $ to nothing - if not _NEEDS_QUOTE.search(tok): - return tok - return _quote_arg(tok) - - def _defines_flags( config: _BuildConfig, flash_mode: str, board: str, board_defines: tuple[str, ...] ) -> list[str]: @@ -301,7 +273,7 @@ def _unflag_tokens() -> set[str]: def _project_flags( - unflags: set[str] | None = None, + unflags: set[str], ) -> tuple[list[str], list[str], list[Path], list[str]]: """Split the ESPHome build flags into compile, linker, -L, and -l lists. @@ -310,8 +282,6 @@ def _project_flags( ``build_unflags`` matches individual tokens (``-Os`` inside ``-Os -g3``). Lexed tokens are re-quoted at emission via ``_shell_token``. """ - if unflags is None: - unflags = _unflag_tokens() compile_flags: list[str] = [] link_flags: list[str] = [] lib_dirs: list[Path] = [] @@ -332,7 +302,7 @@ def _project_flags( def generate_ld_scripts( - paths: dict[str, Path], config: _BuildConfig, flash_ld_name: str + paths: InstalledPaths, config: _BuildConfig, flash_ld_name: str ) -> None: """Generate the common linker script (and testing-mode flash ld copy). @@ -340,8 +310,8 @@ def generate_ld_scripts( ``eagle.app.v6.common.ld.h``, then applies ESPHome's surgeries: the wifi rate-table DRAM relocation, and enlarged memory segments in testing mode. """ - framework = paths["framework_path"] - gcc = paths["toolchain_path"] / "bin" / "xtensa-lx106-elf-gcc" + framework = paths.framework + gcc = paths.toolchain / "bin" / "xtensa-lx106-elf-gcc" ld_dir = CORE.relative_pioenvs_path(CORE.name, "ld") mkdir_p(ld_dir) diff --git a/tests/unit_tests/build_gen/test_arduino8266.py b/tests/unit_tests/build_gen/test_arduino8266.py index d758058549..d11eafa206 100644 --- a/tests/unit_tests/build_gen/test_arduino8266.py +++ b/tests/unit_tests/build_gen/test_arduino8266.py @@ -16,6 +16,7 @@ from unittest.mock import MagicMock, patch import pytest +from esphome.arduino8266.framework import InstalledPaths from esphome.build_gen import arduino8266 from esphome.build_gen.arduino8266 import ( _defines_flags, @@ -171,11 +172,7 @@ def _make_framework(tmp_path: Path) -> dict[str, Path]: toolchain = tmp_path / "toolchain" (toolchain / "bin").mkdir(parents=True) (toolchain / "include").mkdir() - return { - "framework_path": framework, - "toolchain_path": toolchain, - "ninja_path": Path("ninja"), - } + return InstalledPaths(framework=framework, toolchain=toolchain, ninja=Path("ninja")) @pytest.mark.parametrize( @@ -261,7 +258,7 @@ SECTIONS """ -def _run_generate_ld_scripts(paths: dict[str, Path]) -> Path: +def _run_generate_ld_scripts(paths: InstalledPaths) -> Path: config = _resolve_build_config(_flag_defines()) arduino8266.generate_ld_scripts(paths, config, "eagle.flash.4m.ld") @@ -313,10 +310,11 @@ def test_generate_ld_scripts_failure(tmp_path: Path) -> None: def test_generate_ld_scripts_testing_mode(tmp_path: Path) -> None: paths = _make_framework(tmp_path) - (paths["framework_path"] / "tools" / "sdk" / "ld" / "eagle.flash.4m.ld").write_text( + (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" - " iram1_0_seg : org = 0x40100000, len = 0x8000\n" " irom0_0_seg : org = 0x40201010, len = 0xfeff0\n" "}\n" ) @@ -342,7 +340,9 @@ def test_project_flags_trailing_bare_linker_flag_warns( caplog: pytest.LogCaptureFixture, ) -> None: _set_flags("-l") - compile_flags, link_flags, lib_dirs, libs = arduino8266._project_flags() + compile_flags, link_flags, lib_dirs, libs = arduino8266._project_flags( + arduino8266._unflag_tokens() + ) assert "Ignoring trailing '-l'" in caplog.text assert not libs assert not lib_dirs @@ -352,7 +352,9 @@ def test_project_flags_trailing_bare_linker_flag_warns( def test_project_flags_lexed_entry_scatters_non_linker_tokens() -> None: _set_flags("-L /d -Wl,-Map=m stray") - compile_flags, link_flags, lib_dirs, libs = arduino8266._project_flags() + compile_flags, link_flags, lib_dirs, libs = arduino8266._project_flags( + arduino8266._unflag_tokens() + ) assert lib_dirs == [Path("/d")] assert link_flags == ["-Wl,-Map=m"] assert "stray" in compile_flags @@ -372,7 +374,9 @@ def test_flag_defines_lexes_multi_token_entries() -> None: def test_project_flags_lexes_every_entry() -> None: """A linker flag anywhere in an entry reaches the link line (PIO parity).""" _set_flags("-DFOO=1 -lbar") - compile_flags, _link, _dirs, libs = arduino8266._project_flags() + compile_flags, _link, _dirs, libs = arduino8266._project_flags( + arduino8266._unflag_tokens() + ) assert libs == ["bar"] assert "-DFOO=1" in compile_flags @@ -391,7 +395,9 @@ def test_project_flags_unflags_match_tokens() -> None: def test_project_flags_requotes_lexed_defines() -> None: """A quoted spaced value stays one compiler argument after lex/emit.""" _set_flags('-DGREETING="hello world"') - compile_flags, _link, _dirs, _libs = arduino8266._project_flags() + compile_flags, _link, _dirs, _libs = arduino8266._project_flags( + arduino8266._unflag_tokens() + ) # shlex folds the quotes (as PIO's ParseFlags does); _shell_token # re-quotes the spaced token so the shell passes one argv element assert compile_flags == ['"-DGREETING=hello world"']