Adopt the shared ninja quoting helpers and typed install paths in the build spec

This commit is contained in:
J. Nick Koston
2026-08-20 16:07:22 -05:00
parent 57c956dfc3
commit 0171af9f67
2 changed files with 27 additions and 51 deletions
+9 -39
View File
@@ -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)
+18 -12
View File
@@ -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"']