Skip to content

Commit 56307ad

Browse files
authored
feat: support package-owned ldflags
Add package-owned ldflags support and prepare mcpp 0.0.38.
1 parent 4c9e7fa commit 56307ad

10 files changed

Lines changed: 199 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
55
6+
## [0.0.38] — 2026-05-31
7+
8+
### 新增
9+
10+
- 支持包描述拥有自己的 `ldflags`,依赖包声明的链接参数会随包源码编译
11+
一起进入最终链接命令,消费方项目不再需要手动补齐第三方 C/C++
12+
库的私有链接参数。
13+
614
## [0.0.37] — 2026-05-31
715

816
### 修复

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ import mcpplibs.cmdline;
142142
- Ninja 后端:自动生成 build.ninja,并行编译
143143
- compile_commands.json 自动生成(clangd / ccls 即用)
144144
- C 语言一等支持:`.c` 文件自动检测,混合 C/C++ 项目
145-
- 用户自定义 cflags / cxxflags / c_standard
145+
- 用户自定义 cflags / cxxflags / ldflags / c_standard
146146

147147
</details>
148148

docs/05-mcpp-toml.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ include_dirs = ["include", "third_party/include"] # 头文件搜索路径
7373
c_standard = "c11" # C 源文件的标准(默认 c11)
7474
cflags = ["-DFOO=1"] # 额外 C 编译参数
7575
cxxflags = ["-DBAR=2"] # 额外 C++ 编译参数
76+
ldflags = ["-lfoo"] # 额外链接参数
7677
static_stdlib = true # 静态链接 libstdc++(默认 true)
7778
```
7879

mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "0.0.37"
3+
version = "0.0.38"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

src/build/flags.cppm

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,26 @@ std::string escape_path(const std::filesystem::path& p) {
5656
return out;
5757
}
5858

59+
std::string normalize_ldflag(const std::filesystem::path& root, const std::string& flag) {
60+
auto absolute_path = [&](std::string_view raw) {
61+
std::filesystem::path p{std::string(raw)};
62+
if (p.is_absolute() || raw.starts_with("$")) return p;
63+
return root / p;
64+
};
65+
66+
if (flag.starts_with("-L") && flag.size() > 2) {
67+
return "-L" + escape_path(absolute_path(std::string_view(flag).substr(2)));
68+
}
69+
70+
constexpr std::string_view rpathPrefix = "-Wl,-rpath,";
71+
if (flag.starts_with(rpathPrefix) && flag.size() > rpathPrefix.size()) {
72+
return std::string(rpathPrefix)
73+
+ escape_path(absolute_path(std::string_view(flag).substr(rpathPrefix.size())));
74+
}
75+
76+
return flag;
77+
}
78+
5979
} // namespace
6080

6181
CompileFlags compute_flags(const BuildPlan& plan) {
@@ -192,6 +212,11 @@ CompileFlags compute_flags(const BuildPlan& plan) {
192212
};
193213
std::string user_cxxflags = join(plan.manifest.buildConfig.cxxflags);
194214
std::string user_cflags = join(plan.manifest.buildConfig.cflags);
215+
std::string user_ldflags;
216+
for (auto const& flag : plan.manifest.buildConfig.ldflags) {
217+
user_ldflags += ' ';
218+
user_ldflags += normalize_ldflag(plan.projectRoot, flag);
219+
}
195220

196221
// C standard
197222
std::string c_std =
@@ -257,12 +282,12 @@ CompileFlags compute_flags(const BuildPlan& plan) {
257282
}
258283

259284
if constexpr (mcpp::platform::is_windows) {
260-
f.ld = "";
285+
f.ld = user_ldflags;
261286
} else if constexpr (mcpp::platform::needs_explicit_libcxx) {
262-
f.ld = std::format("{}{}{} -lc++", full_static, static_stdlib, b_flag);
287+
f.ld = std::format("{}{}{} -lc++{}", full_static, static_stdlib, b_flag, user_ldflags);
263288
} else {
264-
f.ld = std::format("{}{}{}{}{}{}", full_static, static_stdlib, link_toolchain_flags, b_flag,
265-
runtime_dirs, payload_ld);
289+
f.ld = std::format("{}{}{}{}{}{}{}", full_static, static_stdlib, link_toolchain_flags, b_flag,
290+
runtime_dirs, payload_ld, user_ldflags);
266291
}
267292

268293
return f;

src/cli.cppm

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,10 @@ std::string canonical_compile_flags(const mcpp::manifest::Manifest& m) {
597597
s += " cxxflag:";
598598
s += flag;
599599
}
600+
for (auto const& flag : m.buildConfig.ldflags) {
601+
s += " ldflag:";
602+
s += flag;
603+
}
600604
return s;
601605
}
602606

@@ -623,6 +627,10 @@ std::string canonical_package_build_metadata(
623627
s += " cxxflag:";
624628
s += flag;
625629
}
630+
for (auto const& flag : pkg.manifest.buildConfig.ldflags) {
631+
s += " ldflag:";
632+
s += flag;
633+
}
626634
for (auto const& [path, content] : pkg.manifest.buildConfig.generatedFiles) {
627635
s += " genfile:";
628636
s += path.generic_string();
@@ -1541,6 +1549,7 @@ prepare_build(bool print_fingerprint,
15411549
std::string source; // "version" | "path" | "git" — for type-clash check
15421550
std::size_t depIndex = 0; // index into dep_manifests/packages-1 (for in-place re-fetch)
15431551
std::vector<std::filesystem::path> includeDirsAdded; // entries appended to m->buildConfig.includeDirs by this dep
1552+
std::vector<std::string> linkFlagsAdded; // entries appended to m->buildConfig.ldflags by this dep
15441553
};
15451554
std::map<ResolvedKey, ResolvedRecord> resolved;
15461555

@@ -1824,6 +1833,48 @@ prepare_build(bool print_fingerprint,
18241833
}
18251834
};
18261835

1836+
auto normalizeDepLdflag = [](const std::filesystem::path& depRoot,
1837+
const std::string& flag) {
1838+
auto absolute_path = [&](std::string_view raw) {
1839+
std::filesystem::path p{std::string(raw)};
1840+
if (p.is_absolute() || raw.starts_with("$")) return p;
1841+
return depRoot / p;
1842+
};
1843+
1844+
if (flag.starts_with("-L") && flag.size() > 2) {
1845+
return "-L" + absolute_path(std::string_view(flag).substr(2)).string();
1846+
}
1847+
1848+
constexpr std::string_view rpathPrefix = "-Wl,-rpath,";
1849+
if (flag.starts_with(rpathPrefix) && flag.size() > rpathPrefix.size()) {
1850+
return std::string(rpathPrefix)
1851+
+ absolute_path(std::string_view(flag).substr(rpathPrefix.size())).string();
1852+
}
1853+
1854+
return flag;
1855+
};
1856+
1857+
auto propagateLinkFlags = [&](const std::filesystem::path& depRoot,
1858+
const mcpp::manifest::Manifest& depManifest)
1859+
-> std::vector<std::string>
1860+
{
1861+
std::vector<std::string> added;
1862+
for (auto const& flag : depManifest.buildConfig.ldflags) {
1863+
auto normalized = normalizeDepLdflag(depRoot, flag);
1864+
m->buildConfig.ldflags.push_back(normalized);
1865+
added.push_back(std::move(normalized));
1866+
}
1867+
return added;
1868+
};
1869+
1870+
auto removeLinkFlags = [&](const std::vector<std::string>& flags) {
1871+
auto& ldflags = m->buildConfig.ldflags;
1872+
for (auto const& flag : flags) {
1873+
auto pos = std::find(ldflags.begin(), ldflags.end(), flag);
1874+
if (pos != ldflags.end()) ldflags.erase(pos);
1875+
}
1876+
};
1877+
18271878
// Stage a dep's source files into a fresh directory, rewriting their
18281879
// module / import declarations against `rename`. Used by the multi-
18291880
// version mangling fallback (Level 1) so two cross-major copies of
@@ -2065,6 +2116,7 @@ prepare_build(bool print_fingerprint,
20652116
});
20662117
packages.push_back({secStage, *dep_manifests.back()});
20672118
auto added = propagateIncludeDirs(secStage, *dep_manifests.back());
2119+
auto linkFlagsAdded = propagateLinkFlags(secStage, *dep_manifests.back());
20682120

20692121
ResolvedKey mangledKey{key.ns, mangled};
20702122
resolved[mangledKey] = ResolvedRecord{
@@ -2074,6 +2126,7 @@ prepare_build(bool print_fingerprint,
20742126
.source = "version",
20752127
.depIndex = dep_manifests.size() - 1,
20762128
.includeDirsAdded = std::move(added),
2129+
.linkFlagsAdded = std::move(linkFlagsAdded),
20772130
};
20782131

20792132
mcpp::ui::info("Mangled",
@@ -2136,7 +2189,9 @@ prepare_build(bool print_fingerprint,
21362189
}
21372190

21382191
removeIncludeDirs(it->second.includeDirsAdded);
2192+
removeLinkFlags(it->second.linkFlagsAdded);
21392193
auto added = propagateIncludeDirs(newRoot, newManifest);
2194+
auto linkFlagsAdded = propagateLinkFlags(newRoot, newManifest);
21402195

21412196
// Replace in dep_manifests + packages. depIndex is the slot
21422197
// in dep_manifests; packages = [main, dep_0, dep_1, …], so
@@ -2147,6 +2202,7 @@ prepare_build(bool print_fingerprint,
21472202

21482203
it->second.version = *merged;
21492204
it->second.includeDirsAdded = std::move(added);
2205+
it->second.linkFlagsAdded = std::move(linkFlagsAdded);
21502206
if (it->second.depIndex < dep_cache_identities.size())
21512207
dep_cache_identities[it->second.depIndex].version = *merged;
21522208

@@ -2284,6 +2340,7 @@ prepare_build(bool print_fingerprint,
22842340
// expansion against dep_root) — stash it so a SemVer merge can
22852341
// evict these entries on a re-fetch.
22862342
auto includeDirsAdded = propagateIncludeDirs(dep_root, *dep_manifest);
2343+
auto linkFlagsAdded = propagateLinkFlags(dep_root, *dep_manifest);
22872344

22882345
// Move the manifest into stable storage so we can later look it up
22892346
// by depIndex (the SemVer merger needs to overwrite the slot).
@@ -2307,6 +2364,7 @@ prepare_build(bool print_fingerprint,
23072364
.source = sourceKind,
23082365
.depIndex = dep_manifests.size() - 1,
23092366
.includeDirsAdded = std::move(includeDirsAdded),
2367+
.linkFlagsAdded = std::move(linkFlagsAdded),
23102368
};
23112369

23122370
// Recurse: the dep's own [dependencies] become new worklist items.

src/manifest.cppm

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ struct BuildConfig {
9191
// Empty cStandard → backend default ("c11" today).
9292
std::vector<std::string> cflags;
9393
std::vector<std::string> cxxflags;
94+
std::vector<std::string> ldflags;
9495
std::string cStandard;
9596
};
9697

@@ -628,6 +629,7 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,
628629
if (auto v = doc->get_bool("build.static_stdlib")) m.buildConfig.staticStdlib = *v;
629630
if (auto v = doc->get_string_array("build.cflags")) m.buildConfig.cflags = *v;
630631
if (auto v = doc->get_string_array("build.cxxflags")) m.buildConfig.cxxflags = *v;
632+
if (auto v = doc->get_string_array("build.ldflags")) m.buildConfig.ldflags = *v;
631633
if (auto v = doc->get_string("build.c_standard")) m.buildConfig.cStandard = *v;
632634

633635
// [lib] — library root convention (cargo-style).
@@ -1396,18 +1398,19 @@ synthesize_from_xpkg_lua(std::string_view luaContent,
13961398
}
13971399
cur.consume('}');
13981400
}
1399-
else if (key == "cflags" || key == "cxxflags") {
1401+
else if (key == "cflags" || key == "cxxflags" || key == "ldflags") {
14001402
// `{ "-Dfoo", "-Wall", ... }` — appended to the per-rule baseline
14011403
// by ninja_backend. cflags goes to the C rule (.c files), cxxflags
1402-
// to C++ rule (.cpp/.cc/.cxx/.cppm).
1404+
// to C++ rule (.cpp/.cc/.cxx/.cppm), ldflags to link commands.
14031405
if (!cur.consume('{')) {
14041406
return std::unexpected(ManifestError{
14051407
std::format("expected '{{' after `{} =`", key),
14061408
m.sourcePath, 0, 0});
14071409
}
14081410
cur.skip_ws_and_comments();
14091411
auto& target = (key == "cflags")
1410-
? m.buildConfig.cflags : m.buildConfig.cxxflags;
1412+
? m.buildConfig.cflags
1413+
: (key == "cxxflags" ? m.buildConfig.cxxflags : m.buildConfig.ldflags);
14111414
while (!cur.eof() && cur.peek() != '}') {
14121415
auto s = cur.read_string();
14131416
if (!s.empty()) target.push_back(std::move(s));

src/toolchain/fingerprint.cppm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import mcpp.toolchain.detect;
1818

1919
export namespace mcpp::toolchain {
2020

21-
inline constexpr std::string_view MCPP_VERSION = "0.0.37";
21+
inline constexpr std::string_view MCPP_VERSION = "0.0.38";
2222

2323
struct FingerprintInputs {
2424
Toolchain toolchain;
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/usr/bin/env bash
2+
# requires: gcc
3+
# Package-owned ldflags: a dependency can declare link flags that are applied
4+
# when the consumer binary is linked.
5+
set -e
6+
7+
TMP=$(mktemp -d)
8+
trap "rm -rf $TMP" EXIT
9+
10+
cd "$TMP"
11+
mkdir -p native depLink app/src
12+
13+
cat > native/answer.c <<'EOF'
14+
int dep_link_answer(void) {
15+
return 42;
16+
}
17+
EOF
18+
19+
gcc -c native/answer.c -o native/answer.o
20+
ar rcs native/libanswer.a native/answer.o
21+
NATIVE_DIR="$(pwd)/native"
22+
23+
cat > depLink/mcpp.toml <<EOF
24+
[package]
25+
name = "depLink"
26+
version = "0.1.0"
27+
28+
[build]
29+
ldflags = ["-L${NATIVE_DIR}", "-lanswer"]
30+
EOF
31+
32+
cat > app/src/main.cpp <<'EOF'
33+
extern "C" int dep_link_answer(void);
34+
35+
int main() {
36+
return dep_link_answer() == 42 ? 0 : 1;
37+
}
38+
EOF
39+
40+
cat > app/mcpp.toml <<'EOF'
41+
[package]
42+
name = "app"
43+
version = "0.1.0"
44+
45+
[build]
46+
sources = ["src/*.cpp"]
47+
48+
[targets.app]
49+
kind = "bin"
50+
main = "src/main.cpp"
51+
52+
[dependencies.depLink]
53+
path = "../depLink"
54+
EOF
55+
56+
cd app
57+
"$MCPP" build > build.log 2>&1 || {
58+
cat build.log
59+
echo "build failed"
60+
exit 1
61+
}
62+
63+
"$MCPP" run > run.log 2>&1 || {
64+
cat run.log
65+
echo "run failed"
66+
exit 1
67+
}
68+
69+
ninja_file="$(find target -name build.ninja | head -1)"
70+
[[ -n "$ninja_file" ]] || {
71+
echo "no build.ninja generated"
72+
exit 1
73+
}
74+
75+
grep -q -- "-L${NATIVE_DIR}" "$ninja_file" || {
76+
echo "dep -L flag missing from build.ninja"
77+
cat "$ninja_file"
78+
exit 1
79+
}
80+
grep -q -- "-lanswer" "$ninja_file" || {
81+
echo "dep -l flag missing from build.ninja"
82+
cat "$ninja_file"
83+
exit 1
84+
}
85+
86+
echo "OK"

tests/unit/test_manifest.cpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ version = "0.1.0"
153153
sources = ["src/**/*.{cppm,c}"]
154154
cflags = ["-Wall", "-DFOO=1"]
155155
cxxflags = ["-Wextra"]
156+
ldflags = ["-lfoo", "-Wl,--as-needed"]
156157
c_standard = "c11"
157158
[targets.x]
158159
kind = "lib"
@@ -164,10 +165,13 @@ kind = "lib"
164165
EXPECT_EQ(m->buildConfig.cflags[1], "-DFOO=1");
165166
ASSERT_EQ(m->buildConfig.cxxflags.size(), 1u);
166167
EXPECT_EQ(m->buildConfig.cxxflags[0], "-Wextra");
168+
ASSERT_EQ(m->buildConfig.ldflags.size(), 2u);
169+
EXPECT_EQ(m->buildConfig.ldflags[0], "-lfoo");
170+
EXPECT_EQ(m->buildConfig.ldflags[1], "-Wl,--as-needed");
167171
EXPECT_EQ(m->buildConfig.cStandard, "c11");
168172
}
169173

170-
TEST(SynthesizeFromXpkgLua, CflagsCxxflagsAndCStandard) {
174+
TEST(SynthesizeFromXpkgLua, CflagsCxxflagsLdflagsAndCStandard) {
171175
constexpr auto src = R"(
172176
package = {
173177
spec = "1",
@@ -177,6 +181,7 @@ package = {
177181
sources = { "*/src/*.c" },
178182
cflags = { "-Wall", "-Dunused" },
179183
cxxflags = { "-Wextra" },
184+
ldflags = { "-ltinyc" },
180185
c_standard = "c11",
181186
targets = { ["tinyc"] = { kind = "lib" } },
182187
},
@@ -189,6 +194,8 @@ package = {
189194
EXPECT_EQ(m->buildConfig.cflags[1], "-Dunused");
190195
ASSERT_EQ(m->buildConfig.cxxflags.size(), 1u);
191196
EXPECT_EQ(m->buildConfig.cxxflags[0], "-Wextra");
197+
ASSERT_EQ(m->buildConfig.ldflags.size(), 1u);
198+
EXPECT_EQ(m->buildConfig.ldflags[0], "-ltinyc");
192199
EXPECT_EQ(m->buildConfig.cStandard, "c11");
193200
ASSERT_EQ(m->modules.sources.size(), 1u);
194201
EXPECT_EQ(m->modules.sources[0], "*/src/*.c");

0 commit comments

Comments
 (0)