diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index 147edd7..d046f23 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -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 + { + 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) { @@ -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& 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. @@ -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"); @@ -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/.ddi + // .ddi is placed beside the object: obj/.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 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); @@ -302,8 +386,7 @@ 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) { @@ -311,28 +394,34 @@ std::string emit_ninja_string(const BuildPlan& plan) { } 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); diff --git a/src/manifest.cppm b/src/manifest.cppm index 3f04aee..66604c6 100644 --- a/src/manifest.cppm +++ b/src/manifest.cppm @@ -80,6 +80,13 @@ struct BuildConfig { // time from --static / --target / [target.].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 cflags; + std::vector cxxflags; + std::string cStandard; }; // `[target.]` — per-target overrides. @@ -371,6 +378,9 @@ std::expected 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")) { @@ -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(); diff --git a/src/modgraph/scanner.cppm b/src/modgraph/scanner.cppm index 5a5680b..88b2402 100644 --- a/src/modgraph/scanner.cppm +++ b/src/modgraph/scanner.cppm @@ -190,6 +190,15 @@ std::expected 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; diff --git a/tests/e2e/26_c_language_support.sh b/tests/e2e/26_c_language_support.sh new file mode 100755 index 0000000..6b5b833 --- /dev/null +++ b/tests/e2e/26_c_language_support.sh @@ -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 +#include +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" diff --git a/tests/unit/test_manifest.cpp b/tests/unit/test_manifest.cpp index 1d429f1..48343a0 100644 --- a/tests/unit/test_manifest.cpp +++ b/tests/unit/test_manifest.cpp @@ -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 = {