Skip to content

fix(ninja): link shared libraries through the link_shared rule - #86

Closed
AshrafAhmed9 wants to merge 1 commit into
embeddedos-org:masterfrom
AshrafAhmed9:fix-shared-lib-link-rule
Closed

fix(ninja): link shared libraries through the link_shared rule#86
AshrafAhmed9 wants to merge 1 commit into
embeddedos-org:masterfrom
AshrafAhmed9:fix-shared-lib-link-rule

Conversation

@AshrafAhmed9

@AshrafAhmed9 AshrafAhmed9 commented Aug 30, 2026

Copy link
Copy Markdown

Shared library support landed twice, in two shapes that don't agree, and the
halves were never reconciled.

_write_ninja() defines a link_shared rule:

rule link_shared
  command = $cc -shared $ldflags $in -o $out $libs

Nothing refers to it. The build statement for a shared_library target uses the
plain link rule and pushes the platform flag into that target's ldflags
instead:

ldflags.insert(0, "-dynamiclib" if sys.platform == "darwin" else "-shared")
...
lines.append(f"build {out}: link {' '.join(obj_files)}")

So link_shared is dead code, and since it's emitted unconditionally its
hardcoded -shared shows up in the build.ninja of a project that builds
nothing but static archives.

Each half also brought its own test. tests/ebuild/...::test_shared_library_uses_shared_link_rule
expects : link_shared , tests/unit/...::test_shared_library_gets_shared_flag
expects : link , and test_static_library_unaffected expects no -shared
anywhere. On master all three fail, on every entry in the CI matrix:

FAILED tests/ebuild/test_ninja_backend.py::test_shared_library_uses_shared_link_rule
FAILED tests/unit/test_ninja_backend.py::...::test_shared_library_gets_shared_flag
FAILED tests/unit/test_ninja_backend.py::...::test_static_library_unaffected
3 failed, 198 passed, 1 skipped

I went with the rule rather than deleting it. It produces a correct
LINK_SHARED line in ninja's output instead of labelling a shared link LINK,
and holding the flag in the rule keeps it out of the ldflags of every target
that isn't a shared library. Two details the old rule got wrong, fixed on the
way: it's now emitted only when a shared_library target exists, and the flag
is the platform's spelling rather than a hardcoded -shared, since the matrix
includes macos-13 where it's -dynamiclib.

Two test assertions move with it. tests/unit asserted ": link "; the comment
above it says its purpose is that the target goes through a compiler driver
rather than ar_rule, so it now asserts exactly that. tests/ebuild hardcoded
-shared, which would have failed on the macOS runner as soon as the rule was
reached at all.

Testing

macOS 15, Python 3.14.

$ python -m pytest tests/ -q
201 passed, 1 skipped

Reverting ninja_backend.py alone and keeping the tests reproduces the three
failures above, so the tests do exercise the change. ldflags for a shared
target now contains only what the target and its packages asked for; -L/-l
wiring is unchanged, and test_shared_library_gets_lib_dirs_and_libs still
covers it.

One caveat on the numbers: master doesn't import right now — ebuild/build/dispatch.py
has a SyntaxError and NinjaBackend._object_path is missing, which #78/#75
and #79 address. I ran the suite with those two applied locally to get a
baseline. This branch touches neither file, so it should merge in any order
relative to them.

Shared library support landed twice, in two incompatible shapes, and the
halves were never reconciled.

The generator defines a link_shared rule, but nothing ever refers to it:
the build statement for a shared_library uses the plain link rule and
pushes -shared (or -dynamiclib) into that target's ldflags instead. So
link_shared is dead, and because it is emitted unconditionally its
hardcoded -shared lands in the build.ninja of projects that build nothing
but static archives.

Each half also brought its own test, and they contradict each other.
tests/ebuild asserts the shared library is built by link_shared;
tests/unit asserts it is built by link. Both cannot hold, and on master
three tests fail on every platform in the CI matrix.

Use the rule. It is the one that produces a correct LINK_SHARED line in
ninja's output, and putting the flag in the rule keeps it out of every
static target's ldflags. Emit it only when a shared_library target exists,
so a static-only project's build.ninja does not carry a rule for a link it
never performs, and give it the platform's spelling rather than a
hardcoded -shared, since the matrix includes macos-13.

Two test assertions move with it. The tests/unit one asserted ": link ",
whose stated purpose in its own comment is that the target goes through a
compiler driver rather than ar_rule; that now checks exactly that.
The tests/ebuild one hardcoded -shared and would have failed on the macOS
runner once the rule was reached at all.

The suite goes from 3 failures to 201 passed, 1 skipped.
@AshrafAhmed9
AshrafAhmed9 force-pushed the fix-shared-lib-link-rule branch from 46710db to 8541f29 Compare August 30, 2026 20:38

@srpatcha srpatcha left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You found a real bug, and independently of the person who found it first. Worth
saying that up front, because the conclusion below is that this should probably
close.

The defect you are fixing is genuine: the link_shared rule hardcoded

command = $cc -shared $ldflags $in -o $out $libs

and -shared is not the flag on Darwin. -dynamiclib is correct.

The conflict is a design disagreement, not a merge artifact

#66 fixes the same bug by the opposite route. Rather than correcting the rule's
flag, it deletes the rule, on the grounds that nothing used it:

the emitted link_shared rule was dead — nothing used it — and it hardcoded
-shared, which is wrong on macOS. Shared libraries already link through the
generic link rule with the platform's flag (-dynamiclib/-shared) and the
-L/-l wiring in ldflags.

Its test asserts that outcome directly:

assert ": link " in lib_line
assert f"ldflags = {shared_flag}" in ninja_file
assert "link_shared" not in ninja_file

So the two PRs cannot both land. This one reintroduces a rule #66 removed
on purpose, and #66's suite fails the moment it comes back.

Why I would keep #66's version

Both spell the flag correctly per platform, so that is a wash. The difference is
structural:

  • #66 has one link rule; the platform flag rides in per-target ldflags
    alongside the -L/-l wiring that shared libraries need anyway.
  • This has two rules, plus a conditional emission to avoid writing a rule a
    static-only project never uses.

The conditional emission is careful work — I like that it keeps build.ninja
free of a rule for a link that never happens — but it exists to manage a rule
that does not have to exist. And #66 notes the removal also unblocks
test_static_library_unaffected, which asserts no -shared appears anywhere in
the generated file; a conditionally-emitted rule satisfies that only for as long
as the condition holds.

#66 is also already approved and carries the dispatch.py syntax repair that
master needs (#87), so it will land first regardless.

What I would do

Close this in favour of #66 — but not silently. Two people finding the same
macOS flag bug independently is worth a line in the changelog, and if you want
the fix attributed, say so on #66 and I will make sure it is.

If you would rather keep contributing here, the ninja backend still has open
work: #79 and #80 are both in this file and both conflict with #66 as well.

For the record, verified on #66's branch:

pytest tests/ -k "shared or ninja"    15 passed, 1 skipped

so the surviving path is covered rather than merely asserted-away.

@AshrafAhmed9

Copy link
Copy Markdown
Author

Agreed, closing in favour of #66.

The structural argument is the right one. One link rule with the flag riding in per-target ldflags beats two rules plus a conditional emission whose only job is keeping the second one out of static-only builds. That conditional was me managing a problem that stops existing once the rule does, and I should have read it that way rather than as careful work.

Taking you up on the changelog line, thanks for offering rather than just closing it. I'll have a look at #79 and #80.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants