diff --git a/ebuild/cli/commands.py b/ebuild/cli/commands.py index d16b9ee..b8366e1 100644 --- a/ebuild/cli/commands.py +++ b/ebuild/cli/commands.py @@ -508,15 +508,28 @@ def _selected_board(default: str = "generic") -> str: the documented walk, so the value has to survive from `ebuild new` or `ebuild configure`. """ - path = Path("eos.yaml") + data = _read_yaml_mapping(Path(_EOS_PROJECT_CONFIG)) or {} + system = data.get("system") + board = system.get("board") if isinstance(system, dict) else None + return board or default + + +def _read_yaml_mapping(path: Path) -> Optional[Dict[str, Any]]: + """The top-level mapping in ``path``, or None if there is not one. + + For optional project files read after a build has already succeeded: + a missing, unreadable, malformed or non-mapping file is treated as + absent rather than raised, because callers index into the result and + a traceback there would fail a build that linked. + """ if not path.is_file(): - return default + return None try: import yaml - data = yaml.safe_load(path.read_text(encoding="utf-8")) or {} + data = yaml.safe_load(path.read_text(encoding="utf-8")) except Exception: - return default - return (data.get("system") or {}).get("board") or default + return None + return data if isinstance(data, dict) else None def _build_summary(cfg: "ProjectConfig", compiler, package_paths, log: Logger) -> None: @@ -622,15 +635,7 @@ def _board_config() -> Optional[Dict[str, Any]]: A project that states its part's real capacity should not be measured against the reference part for its family. """ - path = Path("board.yaml") - if not path.is_file(): - return None - try: - import yaml - data = yaml.safe_load(path.read_text(encoding="utf-8")) - except Exception: - return None - return data if isinstance(data, dict) else None + return _read_yaml_mapping(Path("board.yaml")) def _configure_ninja_backend( diff --git a/tests/unit/test_golden_path.py b/tests/unit/test_golden_path.py index 230777d..ce576d7 100644 --- a/tests/unit/test_golden_path.py +++ b/tests/unit/test_golden_path.py @@ -1,6 +1,7 @@ """ebuild init / sim / platforms: the getting-started guide's commands.""" from __future__ import annotations +import shlex import stat import sys import textwrap @@ -15,9 +16,19 @@ def _fake_qemu(tmp_path: Path, body: str) -> str: - """A stand-in qemu: a Python script that prints like a guest would.""" + """A stand-in qemu: a Python script that prints like a guest would. + + Launched through a /bin/sh wrapper rather than a `#!{sys.executable}` + shebang, because a shebang cannot carry an interpreter path containing + spaces (e.g. a checkout under "My Projects/"), and the exec then fails + with ENOENT before the test exercises anything. + """ + guest = tmp_path / "fake_guest.py" + guest.write_text(textwrap.dedent(body), encoding="utf-8") script = tmp_path / "fake-qemu" - script.write_text(f"#!{sys.executable}\n" + textwrap.dedent(body), encoding="utf-8") + script.write_text( + f'#!/bin/sh\nexec {shlex.quote(sys.executable)} {shlex.quote(str(guest))} "$@"\n', + encoding="utf-8") script.chmod(script.stat().st_mode | stat.S_IEXEC) return str(script) diff --git a/tests/unit/test_golden_path_commands.py b/tests/unit/test_golden_path_commands.py index 951ac26..794ec62 100644 --- a/tests/unit/test_golden_path_commands.py +++ b/tests/unit/test_golden_path_commands.py @@ -76,6 +76,19 @@ def test_board_defaults_when_unset(self, tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) assert _selected_board() == "generic" + @pytest.mark.parametrize("text", [ + "- stm32f4\n", # top level is a list + "system: stm32f4\n", # system is a string, not a mapping + "system: {board: [unclosed\n", # not valid YAML + ]) + def test_a_malformed_eos_yaml_falls_back_instead_of_raising( + self, tmp_path, monkeypatch, text): + """`ebuild build` reads the board back after the image has linked, to + print its summary. A traceback there would fail a successful build.""" + monkeypatch.chdir(tmp_path) + Path("eos.yaml").write_text(text, encoding="utf-8") + assert _selected_board() == "generic" + def test_board_without_a_project_is_an_error(self, tmp_path, monkeypatch): """Refuse rather than write an eos.yaml into whatever directory the developer happened to be standing in."""