Skip to content
Merged
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
123 changes: 106 additions & 17 deletions src/build/ninja_backend.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,39 @@ std::filesystem::path mcpp_exe_path() {
return "mcpp"; // fall back to PATH lookup
}

// Derive a sibling C compiler from a C++ compiler binary path. Used so .c
// sources can be compiled by the actual C frontend (cc1), not g++ which
// rejects implicit `void*` conversions and `restrict` etc.
// .../bin/g++ → .../bin/gcc
// .../bin/x86_64-linux-musl-g++ → .../bin/x86_64-linux-musl-gcc
// .../bin/clang++ → .../bin/clang
// .../bin/c++ → .../bin/cc
// If no sibling exists, return "gcc" so PATH lookup is the final fallback
// (this also keeps unit tests that don't touch a real toolchain happy).
std::filesystem::path derive_c_compiler(const std::filesystem::path& cxx) {
auto fname = cxx.filename().string();
auto try_replace = [&](std::string_view from, std::string_view to)
-> std::optional<std::filesystem::path>
{
auto pos = fname.rfind(from);
if (pos == std::string::npos) return std::nullopt;
std::string repl = fname;
repl.replace(pos, from.size(), to);
auto p = cxx.parent_path() / repl;
std::error_code ec;
if (std::filesystem::exists(p, ec)) return p;
return std::nullopt;
};
if (auto p = try_replace("clang++", "clang")) return *p;
if (auto p = try_replace("g++", "gcc")) return *p;
if (auto p = try_replace("c++", "cc")) return *p;
return "gcc";
}

bool is_c_source(const std::filesystem::path& src) {
return src.extension() == ".c";
}

} // namespace

std::string emit_ninja_string(const BuildPlan& plan) {
Expand Down Expand Up @@ -187,9 +220,41 @@ std::string emit_ninja_string(const BuildPlan& plan) {
// TODO(musl-gcc-upstream): remove once musl-gcc@16+ ships.
const char* opt_flag = isMuslTc ? " -Og" : " -O2";

// M5.x: any C sources in the plan? If so we emit a `cc` variable and a
// separate `c_object` rule so .c files are compiled by the C frontend
// (gcc / clang / cc) rather than g++. .c files compiled with g++ get
// routed to cc1plus which rejects C-only constructs (implicit void*
// conversion, `restrict` keyword, etc.) — fatal for libraries like
// mbedtls / openssl.
bool need_c_rule = false;
for (auto& cu : plan.compileUnits) {
if (is_c_source(cu.source)) { need_c_rule = true; break; }
}

// User-supplied flag tails — appended verbatim to per-rule baselines.
auto join_flags = [](const std::vector<std::string>& flags) {
std::string out;
for (auto& f : flags) { out += ' '; out += f; }
return out;
};
std::string user_cxxflags = join_flags(plan.manifest.buildConfig.cxxflags);
std::string user_cflags = join_flags(plan.manifest.buildConfig.cflags);
std::string c_standard = plan.manifest.buildConfig.cStandard.empty()
? std::string{"c11"} : plan.manifest.buildConfig.cStandard;

append(std::format("cxx = {}\n", escape_ninja_path(plan.toolchain.binaryPath)));
append(std::format("cxxflags = -std=c++23 -fmodules{}{}{}{}{}\n",
opt_flag, pic_flag, sysroot_flag, b_flag, include_flags));
append(std::format("cxxflags = -std=c++23 -fmodules{}{}{}{}{}{}\n",
opt_flag, pic_flag, sysroot_flag, b_flag, include_flags,
user_cxxflags));
if (need_c_rule) {
auto cc_path = derive_c_compiler(plan.toolchain.binaryPath);
append(std::format("cc = {}\n", escape_ninja_path(cc_path)));
// C baseline: same opt/pic/sysroot/-B/include layout as cxxflags but
// no -fmodules and -std= goes to a C dialect.
append(std::format("cflags = -std={}{}{}{}{}{}{}\n",
c_standard, opt_flag, pic_flag, sysroot_flag,
b_flag, include_flags, user_cflags));
}
append(std::format("ldflags ={}{}{}{}\n",
full_static, static_stdlib, sysroot_flag, b_flag));
// `ar` for cxx_archive: prefer sandbox absolute path, fall back to PATH.
Expand Down Expand Up @@ -229,6 +294,14 @@ std::string emit_ninja_string(const BuildPlan& plan) {
if (dyndep) append(" restat = 1\n");
append("\n");

if (need_c_rule) {
append("rule c_object\n");
append(" command = $cc $cflags -c $in -o $out\n");
append(" description = CC $out\n");
if (dyndep) append(" restat = 1\n");
append("\n");
}

append("rule cxx_link\n");
append(" command = $cxx $in -o $out $ldflags\n");
append(" description = LINK $out\n\n");
Expand Down Expand Up @@ -275,12 +348,23 @@ std::string emit_ninja_string(const BuildPlan& plan) {
return s;
};

auto pick_rule = [](const std::filesystem::path& src) -> std::string {
auto ext = src.extension();
if (ext == ".cppm") return "cxx_module";
if (ext == ".c") return "c_object";
return "cxx_object";
};

if (dyndep) {
// ── Phase 1: scan edges (one .ddi per TU). ──────────────────────
// .ddi is placed beside the object: obj/<src>.ddi
// .ddi is placed beside the object: obj/<src>.ddi.
// Skip .c files: they have no `import`s and don't need P1689 scan;
// running them through cxx_scan would route them through g++ /
// -fmodules which is exactly what C support is here to avoid.
std::vector<std::string> ddi_paths;
ddi_paths.reserve(plan.compileUnits.size());
for (auto& cu : plan.compileUnits) {
if (is_c_source(cu.source)) continue;
auto ddi = (std::filesystem::path("obj")
/ cu.source.filename()).string() + ".ddi";
ddi_paths.push_back(ddi);
Expand All @@ -302,37 +386,42 @@ std::string emit_ninja_string(const BuildPlan& plan) {
// them from the plan); the dyndep file adds implicit BMI INPUTS
// (the requires) so ninja schedules in the right order.
for (auto& cu : plan.compileUnits) {
std::string rule = (cu.source.extension() == ".cppm")
? "cxx_module" : "cxx_object";
std::string rule = pick_rule(cu.source);

std::string out_line = "build " + escape_ninja_path(cu.object);
if (cu.providesModule) {
out_line += " | " + bmi_path(*cu.providesModule);
}
out_line += std::format(" : {} {}", rule,
escape_ninja_path(cu.source));
// build.ninja.dd is the dyndep file; ninja requires it as an
// implicit input (so it's built before the compile runs).
out_line += " | build.ninja.dd";
out_line += "\n dyndep = build.ninja.dd\n";
if (rule != "c_object") {
// build.ninja.dd is the dyndep file; ninja requires it as an
// implicit input (so it's built before the compile runs).
out_line += " | build.ninja.dd";
out_line += "\n dyndep = build.ninja.dd\n";
} else {
out_line += "\n";
}
append(std::move(out_line));
}
append("\n");
} else {
// ── Static-deps mode (M3.2 and earlier). ────────────────────────
for (auto& cu : plan.compileUnits) {
std::string rule = pick_rule(cu.source);

std::string implicit;
for (auto& imp : cu.imports) {
if (imp == "std" || imp == "std.compat") {
implicit += " gcm.cache/std.gcm";
continue;
// .c files don't `import` modules; skip BMI implicit inputs.
if (rule != "c_object") {
for (auto& imp : cu.imports) {
if (imp == "std" || imp == "std.compat") {
implicit += " gcm.cache/std.gcm";
continue;
}
implicit += " " + bmi_path(imp);
}
implicit += " " + bmi_path(imp);
}

std::string rule = (cu.source.extension() == ".cppm")
? "cxx_module" : "cxx_object";

std::string out_line = "build " + escape_ninja_path(cu.object);
if (cu.providesModule) {
out_line += " " + bmi_path(*cu.providesModule);
Expand Down
33 changes: 33 additions & 0 deletions src/manifest.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ struct BuildConfig {
// time from --static / --target / [target.<triple>].linkage. Wired
// through to ninja backend as the `-static` link flag.
std::string linkage;
// M5.x C-language support. `cflags` / `cxxflags` are appended verbatim
// to the per-rule baseline (see `ninja_backend` cflags / cxxflags).
// `cStandard` controls -std= for the C compile rule (.c files).
// Empty cStandard → backend default ("c11" today).
std::vector<std::string> cflags;
std::vector<std::string> cxxflags;
std::string cStandard;
};

// `[target.<triple>]` — per-target overrides.
Expand Down Expand Up @@ -371,6 +378,9 @@ std::expected<Manifest, ManifestError> parse_string(std::string_view content,

// [build] — backend tunables
if (auto v = doc->get_bool("build.static_stdlib")) m.buildConfig.staticStdlib = *v;
if (auto v = doc->get_string_array("build.cflags")) m.buildConfig.cflags = *v;
if (auto v = doc->get_string_array("build.cxxflags")) m.buildConfig.cxxflags = *v;
if (auto v = doc->get_string("build.c_standard")) m.buildConfig.cStandard = *v;

// [pack] — `mcpp pack` configuration. See docs/35-pack-design.md.
if (auto v = doc->get_string("pack.default_mode")) {
Expand Down Expand Up @@ -1009,6 +1019,29 @@ synthesize_from_xpkg_lua(std::string_view luaContent,
}
cur.consume('}');
}
else if (key == "cflags" || key == "cxxflags") {
// `{ "-Dfoo", "-Wall", ... }` — appended to the per-rule baseline
// by ninja_backend. cflags goes to the C rule (.c files), cxxflags
// to C++ rule (.cpp/.cc/.cxx/.cppm).
if (!cur.consume('{')) {
return std::unexpected(ManifestError{
std::format("expected '{{' after `{} =`", key),
m.sourcePath, 0, 0});
}
cur.skip_ws_and_comments();
auto& target = (key == "cflags")
? m.buildConfig.cflags : m.buildConfig.cxxflags;
while (!cur.eof() && cur.peek() != '}') {
auto s = cur.read_string();
if (!s.empty()) target.push_back(std::move(s));
cur.skip_ws_and_comments();
}
cur.consume('}');
}
else if (key == "c_standard") {
auto v = cur.read_string();
if (!v.empty()) m.buildConfig.cStandard = v;
}
else {
// Unknown key — skip the value (string / bareword / table).
cur.skip_ws_and_comments();
Expand Down
9 changes: 9 additions & 0 deletions src/modgraph/scanner.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,15 @@ std::expected<SourceUnit, ScanError> scan_file(const std::filesystem::path& file
u.path = file;
u.packageName = packageName;

// .c files are pure C: they cannot legally contain `module` / `import`
// declarations, and we route them to the C-language compile rule (no
// P1689 scan, no BMI lookups). Skip the line-by-line module scan to
// avoid any chance of a benign C identifier (`import_foo`, `module_t`,
// ...) being misparsed.
if (file.extension() == ".c") {
return u;
}

int if_depth = 0; // #if/#ifdef nesting
std::size_t lineno = 0;
std::string line;
Expand Down
84 changes: 84 additions & 0 deletions tests/e2e/26_c_language_support.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/usr/bin/env bash
# C-language compile rule: .c files routed to `c_object` with cc / cflags,
# distinct from the .cppm/.cpp `cxx_object` rule. Verifies that a mixed
# C + modular-C++23 project links and runs, and that build.ninja contains
# the expected `c_object` / `cc` / `cflags` plumbing.
set -e

TMP=$(mktemp -d)
trap "rm -rf $TMP" EXIT

cd "$TMP"
"$MCPP" new cmix > /dev/null
cd cmix

# A pure-C source that *requires* the C frontend: uses `restrict` and
# implicit `void* → int*` from malloc(), both rejected by g++ in C++ mode.
cat > src/cmix_core.c <<'EOF'
#include <stdlib.h>
#include <string.h>
int *cmix_dup(const int *restrict src, size_t n) {
int *out = malloc(n * sizeof(int));
if (!out) return 0;
memcpy(out, src, n * sizeof(int));
return out;
}
int cmix_sum(const int *p, size_t n) {
int s = 0;
for (size_t i = 0; i < n; ++i) s += p[i];
return s;
}
EOF

cat > src/main.cpp <<'EOF'
import std;
extern "C" int *cmix_dup(const int *src, std::size_t n);
extern "C" int cmix_sum(const int *p, std::size_t n);
int main() {
int data[] = {1, 2, 3, 4, 5};
int *copy = cmix_dup(data, 5);
int s = cmix_sum(copy, 5);
std::println("cmix sum = {}", s);
std::free(copy);
return s == 15 ? 0 : 1;
}
EOF

# Add user-supplied cflags/cxxflags so we also exercise the flag-forwarding
# path through the manifest into the per-rule baselines.
cat > mcpp.toml <<'EOF'
[package]
name = "cmix"
version = "0.1.0"
[build]
cflags = ["-DCMIX_C_BUILD=1"]
cxxflags = ["-DCMIX_CXX_BUILD=1"]
c_standard = "c11"
EOF

"$MCPP" build > build.log 2>&1 || { cat build.log; echo "build failed"; exit 1; }

ninja_file="$(find target -name build.ninja | head -1)"
[[ -n "$ninja_file" ]] || { echo "no build.ninja generated"; exit 1; }

# c_object rule must be present alongside cxx_object.
grep -q '^rule c_object' "$ninja_file" || { cat "$ninja_file"; echo "missing c_object rule"; exit 1; }
# cc / cflags must be defined and pick a C compiler (gcc / cc / clang).
grep -qE '^cc = .*(gcc|cc|clang)' "$ninja_file" || {
echo "cc variable not pointing at a C compiler"; exit 1; }
grep -qE '^cflags = -std=c11.*-DCMIX_C_BUILD=1' "$ninja_file" || {
echo "cflags missing -std=c11 or user cflags tail"; exit 1; }
grep -qE '^cxxflags = -std=c\+\+23.*-DCMIX_CXX_BUILD=1' "$ninja_file" || {
echo "cxxflags missing -std=c++23 or user cxxflags tail"; exit 1; }
# The .c source must be routed through c_object (not cxx_object).
grep -qE 'build obj/cmix_core\.o : c_object .*cmix_core\.c' "$ninja_file" || {
echo "cmix_core.c not routed to c_object rule"; exit 1; }
grep -qE 'build obj/cmix_core\.o : cxx_object' "$ninja_file" && {
echo "cmix_core.c was incorrectly routed to cxx_object"; exit 1; } || true

# Run the binary; it must print "cmix sum = 15" and exit 0.
out="$("$MCPP" run 2>&1 | tail -1)"
[[ "$out" == "cmix sum = 15" ]] || {
echo "unexpected output: $out"; exit 1; }

echo "OK"
50 changes: 50 additions & 0 deletions tests/unit/test_manifest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,56 @@ TEST(ListXpkgVersions, MissingXpmReturnsEmpty) {
EXPECT_TRUE(mcpp::manifest::list_xpkg_versions(src, "linux").empty());
}

TEST(Manifest, BuildCflagsCxxflagsAndCStandard) {
constexpr auto src = R"(
[package]
name = "x"
version = "0.1.0"
[build]
sources = ["src/**/*.{cppm,c}"]
cflags = ["-Wall", "-DFOO=1"]
cxxflags = ["-Wextra"]
c_standard = "c11"
[targets.x]
kind = "lib"
)";
auto m = mcpp::manifest::parse_string(src);
ASSERT_TRUE(m.has_value()) << m.error().format();
ASSERT_EQ(m->buildConfig.cflags.size(), 2u);
EXPECT_EQ(m->buildConfig.cflags[0], "-Wall");
EXPECT_EQ(m->buildConfig.cflags[1], "-DFOO=1");
ASSERT_EQ(m->buildConfig.cxxflags.size(), 1u);
EXPECT_EQ(m->buildConfig.cxxflags[0], "-Wextra");
EXPECT_EQ(m->buildConfig.cStandard, "c11");
}

TEST(SynthesizeFromXpkgLua, CflagsCxxflagsAndCStandard) {
constexpr auto src = R"(
package = {
spec = "1",
name = "tinyc",
xpm = { linux = { ["1.0.0"] = { url = "u", sha256 = "h" } } },
mcpp = {
sources = { "*/src/*.c" },
cflags = { "-Wall", "-Dunused" },
cxxflags = { "-Wextra" },
c_standard = "c11",
targets = { ["tinyc"] = { kind = "lib" } },
},
}
)";
auto m = mcpp::manifest::synthesize_from_xpkg_lua(src, "tinyc", "1.0.0");
ASSERT_TRUE(m.has_value()) << m.error().format();
ASSERT_EQ(m->buildConfig.cflags.size(), 2u);
EXPECT_EQ(m->buildConfig.cflags[0], "-Wall");
EXPECT_EQ(m->buildConfig.cflags[1], "-Dunused");
ASSERT_EQ(m->buildConfig.cxxflags.size(), 1u);
EXPECT_EQ(m->buildConfig.cxxflags[0], "-Wextra");
EXPECT_EQ(m->buildConfig.cStandard, "c11");
ASSERT_EQ(m->modules.sources.size(), 1u);
EXPECT_EQ(m->modules.sources[0], "*/src/*.c");
}

TEST(ListXpkgVersions, IgnoresCommentedEntries) {
constexpr auto src = R"(
package = {
Expand Down
Loading