From 4f06bb67616e373070b8801a96f90d46274f15cd Mon Sep 17 00:00:00 2001 From: aman-sharma-dev Date: Fri, 11 Sep 2026 16:31:35 +0530 Subject: [PATCH 1/2] fix: avoid ninja object path collisions Signed-off-by: aman-sharma-dev --- TASKS.md | 12 +++++++++++ ebuild/build/ninja_backend.py | 7 ++++-- tests/unit/test_ninja_backend.py | 37 ++++++++++++++++++++++++++++++-- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/TASKS.md b/TASKS.md index 09f6bcb..7ed80a3 100644 --- a/TASKS.md +++ b/TASKS.md @@ -52,6 +52,18 @@ Status is one of: `todo`, `in-progress`, `blocked`, `review`, `done`. ## Completed +### T-006 — Preserve source extensions in Ninja object paths + +- Implementation and independent review complete: `start.c` and `start.S` + produce `start.c.o` and `start.S.o`, retaining target namespaces. +- Focused regression suite: 19 passed, including real Ninja dry runs; before + the fix, 3 failed and 16 passed. Changed-file Ruff and diff checks passed. +- Separate architecture review and changelog entry omitted per ORCHESTRATION.md: + this is a one-line naming fix; the helper documentation describes the change. +- 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. These are outside this fix's scope. + | ID | Task | Owner | Verified by | Evidence | |----|------|-------|-------------|----------| | 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. | diff --git a/ebuild/build/ninja_backend.py b/ebuild/build/ninja_backend.py index de417bd..043564b 100644 --- a/ebuild/build/ninja_backend.py +++ b/ebuild/build/ninja_backend.py @@ -178,11 +178,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 c0dd009..20421d9 100644 --- a/tests/unit/test_ninja_backend.py +++ b/tests/unit/test_ninja_backend.py @@ -182,7 +182,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 @@ -203,8 +207,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") @@ -223,6 +228,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. From d52a9b5bae5238e0349e4ee1b5628e04ddd712f0 Mon Sep 17 00:00:00 2001 From: aman-sharma-dev Date: Mon, 14 Sep 2026 12:52:54 +0530 Subject: [PATCH 2/2] docs: tidy up PR #133 follow-ups Signed-off-by: aman-sharma-dev --- CHANGELOG.md | 5 +++++ TASKS.md | 13 +------------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ba0175..5141c57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +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 +- **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 7ed80a3..626bd1d 100644 --- a/TASKS.md +++ b/TASKS.md @@ -52,22 +52,11 @@ Status is one of: `todo`, `in-progress`, `blocked`, `review`, `done`. ## Completed -### T-006 — Preserve source extensions in Ninja object paths - -- Implementation and independent review complete: `start.c` and `start.S` - produce `start.c.o` and `start.S.o`, retaining target namespaces. -- Focused regression suite: 19 passed, including real Ninja dry runs; before - the fix, 3 failed and 16 passed. Changed-file Ruff and diff checks passed. -- Separate architecture review and changelog entry omitted per ORCHESTRATION.md: - this is a one-line naming fix; the helper documentation describes the change. -- 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. These are outside this fix's scope. - | ID | Task | Owner | Verified by | Evidence | |----|------|-------|-------------|----------| | 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. | ---