From c16d3639497009ebdc7f4d0ee3abf080e6c3d78e Mon Sep 17 00:00:00 2001 From: Abd-ullah2001 Date: Thu, 10 Sep 2026 20:32:36 +0500 Subject: [PATCH] fix(ninja): compile shared libraries with PIC --- CHANGELOG.md | 4 ++ ebuild/build/ninja_backend.py | 22 +++++++--- tests/ebuild/test_ninja_backend.py | 64 ++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ba01756..032b4537 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,10 @@ `cjson` (v1.7.18), `nanopb` (v0.4.9.1), `lvgl` (v9.2.2), `tinyusb` (v0.18.0), and `unity` (v2.6.1). ### Fixed +- **Ninja shared-library sources compile as position-independent code.** + Shared-library targets now default to `-fPIC`, while preserving an explicit + PIC policy supplied by the target or toolchain + (`ebuild/build/ninja_backend.py`). - **`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/ebuild/build/ninja_backend.py b/ebuild/build/ninja_backend.py index de417bdc..961b5a81 100644 --- a/ebuild/build/ninja_backend.py +++ b/ebuild/build/ninja_backend.py @@ -24,10 +24,11 @@ class PackagePaths: libraries: List[str] = field(default_factory=list) -# Flags that already request position-independent code. If one of these is -# present (or explicitly disabled with -fno-*) we must not add -fPIC again. -_PIC_FLAGS = {"-fPIC", "-fpic", "-fPIE", "-fpie", "-fno-pic", "-fno-PIC", - "-fno-pie", "-fno-PIE"} +# Flags that explicitly control PIC generation. PIE is intentionally excluded: +# its output is suitable for executables, not shared libraries. +_PIC_FLAGS = {"-fPIC", "-fpic", "-fno-pic", "-fno-PIC"} +_PIE_FLAGS = {"-fPIE", "-fpie", "-fno-pie", "-fno-PIE"} +_POSITION_INDEPENDENCE_FLAGS = _PIC_FLAGS | _PIE_FLAGS def _exe_suffix() -> str: @@ -146,7 +147,9 @@ def _resolve_target_cflags(self, target) -> List[str]: """Resolve all cflags for a target (toolchain + target + packages). Combines toolchain flags, target-specific flags, include paths, - defines, and package include directories into a single list. + defines, and package include directories into a single list. Shared + library sources default to position-independent code unless the target + or toolchain explicitly selects a PIC policy. Args: target: A TargetConfig with cflags, includes, defines, and uses. @@ -166,6 +169,15 @@ def _resolve_target_cflags(self, target) -> List[str]: for inc_dir in pkg.include_dirs: cflags.append(f"-I{inc_dir}") + if target.target_type == "shared_library": + effective_flag = None + for flag in reversed(cflags): + if flag in _POSITION_INDEPENDENCE_FLAGS: + effective_flag = flag + break + if effective_flag not in _PIC_FLAGS: + cflags.append("-fPIC") + return cflags def _object_path(self, target, src: str) -> Path: diff --git a/tests/ebuild/test_ninja_backend.py b/tests/ebuild/test_ninja_backend.py index d19dd8e9..2e9ef8b5 100644 --- a/tests/ebuild/test_ninja_backend.py +++ b/tests/ebuild/test_ninja_backend.py @@ -62,6 +62,70 @@ def test_shared_library_links_with_the_platform_shared_flag(tmp_path): assert ": link_shared " in ninja_file +def test_shared_library_sources_default_to_fpic(tmp_path): + config = _shared_library_config(tmp_path) + toolchain = SimpleNamespace(cc="cc", cxx="c++", ar="ar") + + NinjaBackend(config, tmp_path / "build", toolchain).generate() + + ninja_file = (tmp_path / "build" / "build.ninja").read_text(encoding="utf-8") + compile_commands = json.loads( + (tmp_path / "build" / "compile_commands.json").read_text(encoding="utf-8") + ) + + assert "-fPIC" in ninja_file + assert "-fPIC" in compile_commands[0]["command"].split() + + +def test_shared_library_respects_explicit_pic_policy(tmp_path): + config = _shared_library_config(tmp_path, target_cflags=["-fno-pic"]) + toolchain = SimpleNamespace(cc="cc", cxx="c++", ar="ar") + + NinjaBackend(config, tmp_path / "build", toolchain).generate() + + compile_commands = json.loads( + (tmp_path / "build" / "compile_commands.json").read_text(encoding="utf-8") + ) + flags = compile_commands[0]["command"].split() + + assert "-fno-pic" in flags + assert "-fPIC" not in flags + + +@pytest.mark.parametrize("pie_flag", ["-fPIE", "-fpie", "-fno-PIE", "-fno-pie"]) +def test_shared_library_does_not_treat_pie_as_pic(tmp_path, pie_flag): + config = _shared_library_config(tmp_path) + toolchain = SimpleNamespace(cc="cc", cxx="c++", ar="ar", cflags=[pie_flag]) + + NinjaBackend(config, tmp_path / "build", toolchain).generate() + + compile_commands = json.loads( + (tmp_path / "build" / "compile_commands.json").read_text(encoding="utf-8") + ) + flags = compile_commands[0]["command"].split() + + assert pie_flag in flags + assert "-fPIC" in flags + + +@pytest.mark.parametrize("pie_flag", ["-fPIE", "-fpie", "-fno-PIE", "-fno-pie"]) +def test_shared_library_respects_the_last_position_independence_flag( + tmp_path, pie_flag +): + config = _shared_library_config(tmp_path, target_cflags=[pie_flag]) + toolchain = SimpleNamespace(cc="cc", cxx="c++", ar="ar", cflags=["-fPIC"]) + + NinjaBackend(config, tmp_path / "build", toolchain).generate() + + compile_commands = json.loads( + (tmp_path / "build" / "compile_commands.json").read_text(encoding="utf-8") + ) + flags = compile_commands[0]["command"].split() + pic_and_pie_flags = [flag for flag in flags if flag in {"-fPIC", pie_flag}] + + assert pic_and_pie_flags == ["-fPIC", pie_flag, "-fPIC"] + + def test_cc_rule_emits_and_consumes_a_depfile(tmp_path): """The compile rule must generate a depfile and tell Ninja to read it.