From e9a719cf9c539f89766c2bc7ab8adeda69af53ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Morais?= <118842104+JoaoMorais03@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:26:10 +0000 Subject: [PATCH] config: reject a non-list packages section instead of dropping it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A mapping or scalar under packages: was silently ignored, so a common YAML mistake produced a build with no package dependencies and no error. Signed-off-by: João Morais <118842104+JoaoMorais03@users.noreply.github.com> --- ebuild/core/config.py | 31 +++++++----- tests/ebuild/test_config_validation.py | 66 ++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 12 deletions(-) diff --git a/ebuild/core/config.py b/ebuild/core/config.py index 3695545..3e87f19 100644 --- a/ebuild/core/config.py +++ b/ebuild/core/config.py @@ -281,18 +281,25 @@ def load_config(config_path: str | Path) -> ProjectConfig: # --- packages section (optional, Phase 2) --- packages: List[PackageDep] = [] - raw_packages = raw.get("packages", []) - - if isinstance(raw_packages, list): - for p in raw_packages: - if isinstance(p, dict): - pkg_name = p.get("name", "") - pkg_version = p.get("version") - - if pkg_name: - packages.append( - PackageDep(name=pkg_name, version=pkg_version) - ) + if "packages" in raw: + raw_packages = raw["packages"] + if not isinstance(raw_packages, list): + raise ConfigError( + "'packages' must be a list of package definitions." + ) + for pkg in raw_packages: + if not isinstance(pkg, dict): + raise ConfigError( + "Invalid package definition: expected a YAML mapping, " + f"got {type(pkg).__name__}." + ) + pkg_name = pkg.get("name", "") + pkg_version = pkg.get("version") + if not pkg_name: + raise ConfigError("Package definition must have a 'name' field.") + if pkg_version is not None and not isinstance(pkg_version, str): + pkg_version = str(pkg_version) + packages.append(PackageDep(name=pkg_name, version=pkg_version)) return ProjectConfig( name=project_name, diff --git a/tests/ebuild/test_config_validation.py b/tests/ebuild/test_config_validation.py index a8b1d7f..2d338bd 100644 --- a/tests/ebuild/test_config_validation.py +++ b/tests/ebuild/test_config_validation.py @@ -96,3 +96,69 @@ 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("invalid_packages", [ + {"name": "zlib", "version": "1.2.13"}, + "zlib", + 42, +]) +def test_packages_must_be_a_list(tmp_path, invalid_packages): + path = write_config( + tmp_path, + {"project": {"name": "demo"}, "packages": invalid_packages}, + ) + + with pytest.raises(ConfigError, match="'packages' must be a list"): + load_config(path) + + +@pytest.mark.parametrize("invalid_item", ["zlib", 42, None]) +def test_package_definition_must_be_mapping(tmp_path, invalid_item): + path = write_config( + tmp_path, + {"project": {"name": "demo"}, "packages": [invalid_item]}, + ) + + with pytest.raises(ConfigError, match="expected a YAML mapping"): + load_config(path) + + +def test_package_definition_requires_name(tmp_path): + path = write_config( + tmp_path, + { + "project": {"name": "demo"}, + "packages": [{"version": "1.2.13"}], + }, + ) + + with pytest.raises(ConfigError, match="must have a 'name'"): + load_config(path) + + +def test_packages_list_is_parsed(tmp_path): + path = write_config( + tmp_path, + { + "project": {"name": "demo"}, + "packages": [ + {"name": "zlib", "version": "1.2.13"}, + {"name": "mbedtls"}, + ], + }, + ) + + config = load_config(path) + + assert len(config.packages) == 2 + assert config.packages[0].name == "zlib" + assert config.packages[0].version == "1.2.13" + assert config.packages[1].name == "mbedtls" + assert config.packages[1].version is None + + +def test_omitted_packages_is_empty(tmp_path): + path = write_config(tmp_path, {"project": {"name": "demo"}}) + config = load_config(path) + assert config.packages == []