diff --git a/CHANGELOG.md b/CHANGELOG.md index c232e87..852f326 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,18 +22,11 @@ `cjson` (v1.7.18), `nanopb` (v0.4.9.1), `lvgl` (v9.2.2), `tinyusb` (v0.18.0), and `unity` (v2.6.1). ### Fixed -- **`ebuild analyze --llm` no longer reports success when the model call failed.** - `LLMClient` always probed `http://localhost:11434` for Ollama availability, - even when constructed with a different `base_url`; OpenAI-compatible URLs - that already ended in `/v1` were joined to `/v1/v1/chat/completions`; - `file://` (and other non-HTTP schemes) could reach `urllib.request.urlopen`; - an empty or failed response still made `ebuild analyze --llm` print - "LLM analysis complete". Availability now probes the configured host, - `/v1` is joined once, non-HTTP(S) endpoints are rejected, responses are - capped at 10 MB, error text redacts bearer tokens, and a failed call - records `llm_failed:` so the CLI warns and continues with the - rule-engine profile (`ebuild/eos_ai/llm_integration.py`, - `ebuild/eos_ai/eos_hw_analyzer.py`, `ebuild/cli/commands.py`). +- **Ninja object paths retain source extensions.** Sources such as + `src/start.c` and `src/start.S` now generate distinct objects under + `obj//` (`start.c.o` and `start.S.o`) instead of colliding at + `start.o`. Clean existing build directories once after upgrading so static + archives cannot retain objects named by the previous layout. - **`ebuild test` now finds Windows test binaries.** The Ninja edge for a native `type: test` target already carried the platform suffix (`_exe_suffix()` names it `.exe` on Windows), but `ebuild test` diff --git a/TASKS.md b/TASKS.md index d15945a..44f3ee1 100644 --- a/TASKS.md +++ b/TASKS.md @@ -68,6 +68,7 @@ Status is one of: `todo`, `in-progress`, `blocked`, `review`, `done`. |----|------|-------|-------------|----------| | T-001 | Make initramfs creation portable and self-contained | backend | independent reviewer | Focused archive tests: **5 passed, 1 skipped** (symlink creation unavailable on this Windows host). Independent `bsdtar` extraction validated hard-link identity and payload. Full Python suite: **288 passed, 2 skipped, 1 unrelated failure** in the pre-existing Windows Ninja path assertion, recorded as T-002. QEMU boot was not run on Windows. | | T-003 | Address PR #111 review findings 1, 2, 3, 6, 7, 8 | backend | reviewer | Unit tests in `tests/unit/test_index_sync.py` (22 passed) verify keep-set filename matching, empty index floor and fallback status, .yml preservation and pruning, lack-of-URL recipe preservation, CLI prune reporting, and trailing newline in `CHANGELOG.md`. Static analysis with `ruff` on all PR-touched files reports 0 errors; `mypy` on modified packages reports 0 errors without suppressions. Full test suite passes 605 tests on Linux. | +| T-006 | Preserve source extensions in Ninja object paths | backend | independent reviewer | `start.c` and `start.S` produce `start.c.o` and `start.S.o` within the target namespace. Focused regression suite: **19 passed**, including real Ninja dry runs; before the fix, **3 failed, 16 passed**. Changed-file Ruff and diff checks passed. Full suite: **667 passed, 10 failed, 7 skipped**; the same 10 failures reproduce with the original backend (nine missing `PackageRecipe.to_dict()` errors and one environment permission error). Existing build directories must be cleaned once because static archives can retain objects named by the previous layout; durable archive recreation is tracked by #136 / PR #137. | --- diff --git a/ebuild/build/ninja_backend.py b/ebuild/build/ninja_backend.py index 7515012..098acd3 100644 --- a/ebuild/build/ninja_backend.py +++ b/ebuild/build/ninja_backend.py @@ -147,11 +147,14 @@ def _object_path(self, target, src: str) -> Path: source made both targets claim one output, which ninja rejects with "multiple rules generate ...". + Keep the source extension too: start.c and start.S in one target + must produce start.c.o and start.S.o rather than sharing start.o. + Example: >>> backend._object_path(target, "src/main.c") # target.name == "app" - PosixPath('_build/obj/app/src/main.o') + PosixPath('_build/obj/app/src/main.c.o') """ - return (self.build_dir / "obj" / target.name / src).with_suffix(".o") + return self.build_dir / "obj" / target.name / (src + ".o") def _write_ninja(self) -> None: """Write the build.ninja file.""" diff --git a/tests/unit/test_ninja_backend.py b/tests/unit/test_ninja_backend.py index a41a340..a01d550 100644 --- a/tests/unit/test_ninja_backend.py +++ b/tests/unit/test_ninja_backend.py @@ -190,7 +190,11 @@ def test_shared_source_gets_one_object_per_target(self, tmp_path): assert "-DBUILD_LIB=1" in ninja_content assert "-DBUILD_APP=1" in ninja_content - def test_shared_source_manifest_is_valid_ninja(self, tmp_path, monkeypatch): + @pytest.mark.parametrize("app_sources", [ + ["src/main.c", "src/util.c"], + ["src/util.c", "src/util.S"], + ]) + def test_shared_source_manifest_is_valid_ninja(self, tmp_path, monkeypatch, app_sources): """The generated manifest must load in real ninja, not just look right. Ninja treats two edges producing one output as an error, so this is @@ -211,8 +215,9 @@ def test_shared_source_manifest_is_valid_ninja(self, tmp_path, monkeypatch): "int main(void) { return util_answer() == 42 ? 0 : 1; }\n", encoding="utf-8", ) + (src_dir / "util.S").write_text("", encoding="utf-8") - config = self._shared_source_config(tmp_path, ["src/main.c", "src/util.c"]) + config = self._shared_source_config(tmp_path, app_sources) monkeypatch.chdir(tmp_path) build_dir = Path("_build") @@ -231,6 +236,34 @@ def test_shared_source_manifest_is_valid_ninja(self, tmp_path, monkeypatch): f"{result.stdout}\n{result.stderr}" ) + @pytest.mark.parametrize("sources", [["src/main.c"], ["src/start.c", "src/start.S"]]) + def test_object_outputs_preserve_source_extensions(self, tmp_path, sources): + """Distinct source filenames must stay distinct in both generated files.""" + config = ProjectConfig( + name="source_extensions", version="1.0", source_dir=tmp_path, + targets=[TargetConfig(name="app", target_type="executable", sources=sources)], + ) + build_dir = tmp_path / "_build" + NinjaBackend(config, build_dir, ResolvedToolchain()).generate() + + manifest = (build_dir / "build.ninja").read_text(encoding="utf-8") + compile_edges = [line for line in manifest.splitlines() if ": cc " in line] + objects = [build_dir / "obj" / "app" / (src + ".o") for src in sources] + assert compile_edges == [ + f"build {escape_ninja_path(obj)}: cc {escape_ninja_path(src)}" + for src, obj in zip(sources, objects) + ] + link_edge = next(line for line in manifest.splitlines() if ": link " in line) + assert link_edge.split(": link ", 1)[1] == " ".join( + escape_ninja_path(obj) for obj in objects + ) + + commands = json.loads((build_dir / "compile_commands.json").read_text(encoding="utf-8")) + assert [entry["file"] for entry in commands] == sources + assert [entry["command"].split(" -o ", 1)[1] for entry in commands] == [ + str(obj) for obj in objects + ] + def test_compile_commands_distinguishes_shared_source_entries(self, tmp_path): """compile_commands.json entries for a shared source must differ.