Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<target>/` (`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 `<name>.exe` on Windows), but `ebuild test`
Expand Down
1 change: 1 addition & 0 deletions TASKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,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. |

---

Expand Down
7 changes: 5 additions & 2 deletions ebuild/build/ninja_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
37 changes: 35 additions & 2 deletions tests/unit/test_ninja_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand All @@ -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.

Expand Down
Loading