From d6c0a62f7e94aa9339da5608e79e1378b3df2e9b Mon Sep 17 00:00:00 2001 From: Swayam Nayak <154440440+swayam-2003@users.noreply.github.com> Date: Sun, 30 Aug 2026 21:20:11 +0530 Subject: [PATCH] Validate toolchain extra_cflags/extra_ldflags as string lists Scalar YAML values such as extra_cflags: -O2 were accepted as strings and later iterated per-character into invalid compiler flags. Reject non-list values at config load time with a clear ConfigError, matching existing target-field validation. Signed-off-by: Swayam Nayak <154440440+swayam-2003@users.noreply.github.com> --- CHANGELOG.md | 1 + ebuild/core/config.py | 20 +++++++++++++++++-- tests/ebuild/test_config_validation.py | 27 ++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e52b6a89..20d35882 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [Unreleased] ### Fixed +- **`load_config`:** `toolchain.extra_cflags` and `toolchain.extra_ldflags` must be YAML lists of strings so scalar values cannot silently split into per-character compiler flags. - **Ninja backend: header changes now trigger a rebuild.** The generated `cc` rule declared no depfile, so Ninja only knew about the sources listed in `build.yaml`. Editing a header left stale object files in place and the build diff --git a/ebuild/core/config.py b/ebuild/core/config.py index 9e00f308..6f95475e 100644 --- a/ebuild/core/config.py +++ b/ebuild/core/config.py @@ -165,13 +165,29 @@ def _parse_target(raw: Any) -> TargetConfig: def _parse_toolchain(raw: Dict[str, Any]) -> ToolchainConfig: """Parse toolchain section into a ToolchainConfig.""" + extra_cflags = raw.get("extra_cflags", []) + extra_ldflags = raw.get("extra_ldflags", []) + + for field_name, value in ( + ("extra_cflags", extra_cflags), + ("extra_ldflags", extra_ldflags), + ): + if not isinstance(value, list): + raise ConfigError( + f"Toolchain field '{field_name}' must be a list." + ) + if not all(isinstance(item, str) for item in value): + raise ConfigError( + f"Toolchain field '{field_name}' must contain only strings." + ) + return ToolchainConfig( compiler=raw.get("compiler", "gcc"), arch=raw.get("arch", "x86_64"), prefix=raw.get("prefix"), sysroot=raw.get("sysroot"), - extra_cflags=raw.get("extra_cflags", []), - extra_ldflags=raw.get("extra_ldflags", []), + extra_cflags=extra_cflags, + extra_ldflags=extra_ldflags, ) diff --git a/tests/ebuild/test_config_validation.py b/tests/ebuild/test_config_validation.py index a8b1d7f8..3fec61d7 100644 --- a/tests/ebuild/test_config_validation.py +++ b/tests/ebuild/test_config_validation.py @@ -96,3 +96,30 @@ def test_toolchain_mapping_is_parsed(tmp_path): assert config.toolchain.sysroot == "/opt/arm-none-eabi" assert config.toolchain.extra_cflags == ["-mcpu=cortex-m4"] assert config.toolchain.extra_ldflags == ["--specs=nosys.specs"] + + +@pytest.mark.parametrize("field_name", ["extra_cflags", "extra_ldflags"]) +def test_toolchain_flag_fields_must_be_lists(tmp_path, field_name): + path = write_config( + tmp_path, + { + "project": {"name": "demo"}, + "toolchain": {field_name: "-O2"}, + }, + ) + + with pytest.raises(ConfigError, match=rf"field '{field_name}' must be a list"): + load_config(path) + + +def test_toolchain_flag_items_must_be_strings(tmp_path): + path = write_config( + tmp_path, + { + "project": {"name": "demo"}, + "toolchain": {"extra_cflags": ["-O2", 42]}, + }, + ) + + with pytest.raises(ConfigError, match="extra_cflags.*must contain only strings"): + load_config(path)