Skip to content

Commit ca277ec

Browse files
committed
fix(pack): a shared library package must carry both of the library's names
Found by running the path the docs already promised. `mcpp pack <shared target>` shipped only the built file — `libmathkit-shared.so` — while the object records `SONAME libmathkit.so.1`. A consumer links by the first name and the loader asks for the second, so the package linked and the program could not start. mcpp's own runtime-closure check is what reported it, naming the missing soname rather than letting it become a loader error at launch. The package now carries the SONAME alongside the link name (a symlink, falling back to a copy), which is what a distribution ships and what the design's "soname gives the correct run-time name" note always meant. e2e 251 uses `run`, not `build`: linking proves nothing here.
1 parent e36ecda commit ca277ec

5 files changed

Lines changed: 121 additions & 2 deletions

File tree

docs/12-binary-distribution.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ mathkit-0.1.0-x86_64-linux-gnu-gcc16-libstdcxx16-c++23/
7474
`lib/` is keyed by **triple**, not by OS. MinGW and MSVC are both Windows and
7575
produce `libfoo.a` and `foo.lib` respectively.
7676

77+
A **shared** package carries the library under *both* of its names: a consumer
78+
links `lib<target>.so` and the loader then asks for the `SONAME`, and those are
79+
different filenames. Shipping only the built file links cleanly and then fails
80+
to start.
81+
7782
### Why neither set can be trimmed
7883

7984
A **source** distribution of the same package puts every one of its
@@ -256,7 +261,7 @@ you publish to a mixed audience.
256261
| | status |
257262
|---|---|
258263
| `kind = "lib"` (static) | ✅ every target |
259-
| `kind = "shared"` on Linux/ELF ||
264+
| `kind = "shared"` on Linux/ELF |— the package carries both the link name and the SONAME |
260265
| `kind = "shared"` on PE / Mach-O | ❌ refused — import libraries and install-names are not modelled yet |
261266
| `kind = "shared"` on `*-musl` | ❌ a musl target links statically |
262267
| shipping prebuilt BMIs | ❌ not attempted; BMIs are compiler-build-exact |

docs/zh/12-binary-distribution.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ ldflags = ["-Llib/x86_64-linux-musl", "-lmathkit"]
236236
| | 状态 |
237237
|---|---|
238238
| `kind = "lib"`(静态) | ✅ 所有 target |
239-
| `kind = "shared"` on Linux/ELF ||
239+
| `kind = "shared"` on Linux/ELF |—— 包里同时带链接名与 SONAME |
240240
| `kind = "shared"` on PE / Mach-O | ❌ 拒绝 —— 导入库与 install-name 尚未建模 |
241241
| `kind = "shared"` on `*-musl` | ❌ musl target 是静态链接的 |
242242
| 发布预编译 BMI | ❌ 未尝试;BMI 与编译器构建逐位绑定 |

src/pack/library.cppm

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ struct LibraryLeg {
5151
std::string abiTag;
5252
std::string buildKey;
5353
std::string linkName; // the -l argument, e.g. "mathkit"
54+
// The SONAME the artifact declares, when it declares one. A shared library
55+
// is FOUND at run time by this name and LINKED by `lib<linkName>.so`, and
56+
// those are two different filenames — so a package that ships only the
57+
// built file links fine and then cannot start.
58+
std::string soname;
5459
bool shared = false;
5560
};
5661

@@ -184,6 +189,26 @@ run_library_pack(const LibraryPackPlan& plan)
184189
auto dst = plan.stagingRoot / "lib" / leg.triple / name;
185190
if (auto r = copy_into(leg.artifact, dst); !r) return std::unexpected(r.error());
186191

192+
// A shared library needs BOTH of its names present.
193+
//
194+
// `-lmathkit-shared` resolves `libmathkit-shared.so` at link time, but
195+
// the object records `SONAME libmathkit.so.1`, and that is the name the
196+
// loader asks for. Ship only the built file and the consumer links,
197+
// then fails to start — mcpp's own runtime-closure check reports
198+
// "libmathkit.so.1 not found on the search path this artifact will
199+
// actually use", which is how this was caught.
200+
//
201+
// A symlink is what a distribution ships; a copy is the fallback for
202+
// filesystems (and archives) that cannot carry one.
203+
if (leg.shared && !leg.soname.empty() && leg.soname != name) {
204+
auto alias = dst.parent_path() / leg.soname;
205+
std::error_code linkEc;
206+
std::filesystem::remove(alias, linkEc);
207+
std::filesystem::create_symlink(name, alias, linkEc);
208+
if (linkEc)
209+
if (auto r = copy_into(leg.artifact, alias); !r) return std::unexpected(r.error());
210+
}
211+
187212
// Delete the objects of the units published as source. The consumer
188213
// compiles those itself; leaving them in the archive means two
189214
// definitions of the module initialiser, resolved by link order.

src/pack/library_pipeline.cppm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ export int build_and_pack_library(const std::string& targetName,
268268
.abiTag = tag.str(),
269269
.buildKey = ctx->fp.hex,
270270
.linkName = targetName,
271+
.soname = target->soname,
271272
.shared = shared,
272273
});
273274
mcpp::ui::status("Packed leg", std::format("{} [{}]", triple, tag.str()));
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#!/usr/bin/env bash
2+
# requires: elf gcc
3+
# 251_pack_library_shared.sh — a `kind = "shared"` package carries BOTH of the
4+
# library's names, and a consumer can actually start.
5+
#
6+
# A shared library is LINKED by `lib<target>.so` and FOUND at run time by its
7+
# SONAME, and those are two different filenames. The first version of this
8+
# packer shipped only the built file: the consumer linked, and then mcpp's own
9+
# runtime-closure check reported
10+
#
11+
# libmathkit.so.1 not found on the search path this artifact will actually use
12+
#
13+
# which is the good outcome only because that check exists. Without it the
14+
# program would have failed to start with a loader error naming a file the user
15+
# never asked for.
16+
set -e
17+
18+
TMP=$(mktemp -d)
19+
trap "rm -rf $TMP" EXIT
20+
cd "$TMP"
21+
22+
mkdir -p mathkit/src
23+
cat > mathkit/src/mathkit.cppm <<'EOF'
24+
export module mathkit;
25+
export namespace mk { int answer(); }
26+
EOF
27+
cat > mathkit/src/impl.cpp <<'EOF'
28+
module mathkit;
29+
namespace mk { int answer() { return 42; } }
30+
EOF
31+
cat > mathkit/mcpp.toml <<'EOF'
32+
[package]
33+
name = "mathkit"
34+
version = "0.1.0"
35+
[build]
36+
sources = ["src/*.cppm", "src/*.cpp"]
37+
[targets.mathkit-shared]
38+
kind = "shared"
39+
soname = "libmathkit.so.1"
40+
EOF
41+
42+
cd mathkit
43+
"$MCPP" pack mathkit-shared > pack.log 2>&1 || { cat pack.log; echo "shared pack failed"; exit 1; }
44+
pkg="$TMP/mathkit/$(find target/dist -maxdepth 1 -type d -name 'mathkit-0.1.0-*' | head -1)"
45+
46+
libdir="$(dirname "$(find "$pkg/lib" -name 'libmathkit-shared.so' | head -1)")"
47+
[[ -n "$libdir" ]] || { echo "no .so in the package"; find "$pkg" -type f; exit 1; }
48+
# Both names. The SONAME one may be a symlink or a copy — either is fine, its
49+
# absence is not.
50+
[[ -e "$libdir/libmathkit.so.1" ]] || {
51+
echo "FAIL: the package does not carry the SONAME the loader will ask for"
52+
ls -l "$libdir"; exit 1; }
53+
54+
# The manifest declares it as a shared library and gives a runtime search dir:
55+
# link_library_dirs is not rpath, and a shared package needs both.
56+
grep -q 'role *= *"shared-library"' "$pkg/mcpp.toml" || {
57+
cat "$pkg/mcpp.toml"; echo "artifact is not recorded as a shared library"; exit 1; }
58+
grep -q 'runtime_search_dirs' "$pkg/mcpp.toml" || {
59+
cat "$pkg/mcpp.toml"; echo "no runtime_search_dirs — the consumer could not find it"; exit 1; }
60+
61+
cd "$TMP"
62+
mkdir -p app/src
63+
cat > app/src/main.cpp <<'EOF'
64+
#include <cstdio>
65+
import mathkit;
66+
int main(){ std::printf("ok=%d\n", mk::answer()); return 0; }
67+
EOF
68+
cat > app/mcpp.toml <<EOF
69+
[package]
70+
name = "app"
71+
version = "0.1.0"
72+
[dependencies]
73+
mathkit = { path = "$pkg" }
74+
[targets.app]
75+
kind = "bin"
76+
main = "src/main.cpp"
77+
EOF
78+
79+
# `run`, not `build`: linking proves nothing here — the whole point is that the
80+
# process starts and the loader resolves the SONAME.
81+
( cd app && "$MCPP" run > run.log 2>&1 ) || { cat app/run.log; echo "consumer failed to run"; exit 1; }
82+
grep -q 'ok=42' app/run.log || { cat app/run.log; echo "wrong answer"; exit 1; }
83+
84+
exe="$(find app/target -name app -type f | head -1)"
85+
readelf -d "$exe" 2>/dev/null | grep -q 'libmathkit.so.1' || {
86+
readelf -d "$exe"; echo "the consumer does not NEED the soname"; exit 1; }
87+
88+
echo "PASS: a shared library package carries both names and the consumer starts"

0 commit comments

Comments
 (0)