diff --git a/src/build/execute.cppm b/src/build/execute.cppm index 4dbe49d0..5df9627b 100644 --- a/src/build/execute.cppm +++ b/src/build/execute.cppm @@ -380,8 +380,9 @@ bool sources_newer_than(const std::filesystem::path& projectRoot, } for (auto& f : mcpp::modgraph::expand_glob(projectRoot, "src/**/*")) { auto ext = f.extension().string(); - if (ext != ".cppm" && ext != ".cpp" && ext != ".cc" && - ext != ".cxx" && ext != ".c" && ext != ".h" && ext != ".hpp") + if (ext != ".cppm" && ext != ".ccm" && ext != ".cxxm" && ext != ".ixx" && + ext != ".cpp" && ext != ".cc" && ext != ".cxx" && ext != ".c" && + ext != ".h" && ext != ".hpp") continue; auto ft = std::filesystem::last_write_time(f, ec); if (ec || ft > ninjaTime) return true; diff --git a/src/build/plan.cppm b/src/build/plan.cppm index 8f1133fc..d6a5e973 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -155,8 +155,8 @@ std::string object_filename_for(const std::filesystem::path& src, return src.filename().string() + std::string(objExt); } auto stem = src.stem().string(); - // distinguish .cppm vs .cpp by extension prefix to avoid collisions - return stem + (ext == ".cppm" + // distinguish .cppm/.ccm/.cxxm/.ixx vs .cpp/.cc/.cxx by extension prefix + return stem + ((ext == ".cppm" || ext == ".ccm" || ext == ".cxxm" || ext == ".ixx") ? ".m" + std::string(objExt) : std::string(objExt)); } @@ -751,7 +751,7 @@ make_plan(const mcpp::manifest::Manifest& manifest, auto append_package_objects = [&](LinkUnit& lu, const std::string& packageName) { for (auto& cu : plan.compileUnits) { if (cu.packageName != packageName) continue; - if (cu.source.extension() == ".cppm") { + if (cu.providesModule) { lu.objects.push_back(cu.object); } } @@ -811,7 +811,7 @@ make_plan(const mcpp::manifest::Manifest& manifest, // For binary target, also include main.cpp's object if main is present. for (auto& cu : plan.compileUnits) { if (sharedDepPackages.contains(cu.packageName)) continue; - if (cu.source.extension() == ".cppm") { + if (cu.providesModule) { lu.objects.push_back(cu.object); } } diff --git a/tests/e2e/152_module_extensions.sh b/tests/e2e/152_module_extensions.sh new file mode 100644 index 00000000..0bc9e63e --- /dev/null +++ b/tests/e2e/152_module_extensions.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# requires: elf gcc +# All module interface extensions (.cppm .ccm .cxxm .ixx) must compile and link +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT + +cd "$TMP" +"$MCPP" new moduleext > /dev/null +cd moduleext + +# ---- module interface sources (one per extension) ---- +cat > src/greet.cppm <<'EOF' +export module moduleext.greet; +import std; +export auto greet(std::string_view who) -> void { + std::println("Hello from .cppm, {}!", who); +} +EOF + +cat > src/info.ccm <<'EOF' +export module moduleext.info; +import std; +export auto info(std::string_view what) -> void { + std::println("Info from .ccm: {}", what); +} +EOF + +cat > src/stat.cxxm <<'EOF' +export module moduleext.stat; +import std; +export auto stat(int n) -> void { + std::println("Stat from .cxxm: {}", n); +} +EOF + +cat > src/data.ixx <<'EOF' +export module moduleext.data; +import std; +export auto data(std::string_view key) -> void { + std::println("Data from .ixx: {}", key); +} +EOF + +# ---- consumer ---- +cat > src/main.cpp <<'EOF' +import std; +import moduleext.greet; +import moduleext.info; +import moduleext.stat; +import moduleext.data; +int main() { + greet("world"); + info("build"); + stat(42); + data("key"); + std::println("All module extensions OK"); + return 0; +} +EOF + +# Default glob only picks up .cppm/.cpp/.cc/.c — explicitly add .ccm/.cxxm/.ixx +cat > mcpp.toml <<'EOF' +[package] +name = "moduleext" +version = "0.1.0" +[modules] +sources = ["src/**/*.cppm", "src/**/*.ccm", "src/**/*.cxxm", "src/**/*.ixx", "src/**/*.cpp"] +EOF + +# ---- build & run ---- +"$MCPP" build > build.log 2>&1 || { + cat build.log + echo "FAIL: build failed" + exit 1 +} + +out="$("$MCPP" run 2>&1)" +[[ "$out" == *"from .cppm"* ]] || { echo "module greet (.cppm) not invoked: $out"; exit 1; } +[[ "$out" == *"from .ccm"* ]] || { echo "module info (.ccm) not invoked: $out"; exit 1; } +[[ "$out" == *"from .cxxm"* ]] || { echo "module stat (.cxxm) not invoked: $out"; exit 1; } +[[ "$out" == *"from .ixx"* ]] || { echo "module data (.ixx) not invoked: $out"; exit 1; } +[[ "$out" == *"All module extensions OK"* ]] || { echo "final message missing: $out"; exit 1; } + +# ---- verify build.ninja ---- +build_ninja="$(find target -name build.ninja | head -1)" +[[ -n "$build_ninja" ]] || { echo "no build.ninja generated"; exit 1; } + +# 4 BMIs +grep -q "gcm.cache/moduleext.greet.gcm" "$build_ninja" || { echo "ninja missing greet BMI"; exit 1; } +grep -q "gcm.cache/moduleext.info.gcm" "$build_ninja" || { echo "ninja missing info BMI"; exit 1; } +grep -q "gcm.cache/moduleext.stat.gcm" "$build_ninja" || { echo "ninja missing stat BMI"; exit 1; } +grep -q "gcm.cache/moduleext.data.gcm" "$build_ninja" || { echo "ninja missing data BMI"; exit 1; } + +# 4 .m.o objects (one per module interface extension) +grep -q "greet.m.o" "$build_ninja" || { echo "ninja missing greet.m.o (.cppm)"; exit 1; } +grep -q "info.m.o" "$build_ninja" || { echo "ninja missing info.m.o (.ccm)"; exit 1; } +grep -q "stat.m.o" "$build_ninja" || { echo "ninja missing stat.m.o (.cxxm)"; exit 1; } +grep -q "data.m.o" "$build_ninja" || { echo "ninja missing data.m.o (.ixx)"; exit 1; } + +echo "OK" diff --git a/tests/unit/test_module_extensions.cpp b/tests/unit/test_module_extensions.cpp new file mode 100644 index 00000000..9133778a --- /dev/null +++ b/tests/unit/test_module_extensions.cpp @@ -0,0 +1,34 @@ +#include + +import std; +import mcpp.build.plan; + +using namespace mcpp::build; + +TEST(ModuleExtensions, CppmGetsDotMPrefix) { + EXPECT_EQ(object_filename_for("src/foo.cppm", ".o"), "foo.m.o"); +} + +TEST(ModuleExtensions, CcmGetsDotMPrefix) { + EXPECT_EQ(object_filename_for("src/foo.ccm", ".o"), "foo.m.o"); +} + +TEST(ModuleExtensions, CxxmGetsDotMPrefix) { + EXPECT_EQ(object_filename_for("src/foo.cxxm", ".o"), "foo.m.o"); +} + +TEST(ModuleExtensions, IxxGetsDotMPrefix) { + EXPECT_EQ(object_filename_for("src/foo.ixx", ".o"), "foo.m.o"); +} + +TEST(ModuleExtensions, CppHasNoPrefix) { + EXPECT_EQ(object_filename_for("src/foo.cpp", ".o"), "foo.o"); +} + +TEST(ModuleExtensions, CcHasNoPrefix) { + EXPECT_EQ(object_filename_for("src/foo.cc", ".o"), "foo.o"); +} + +TEST(ModuleExtensions, AsmKeepsFullExt) { + EXPECT_EQ(object_filename_for("src/foo.S", ".o"), "foo.S.o"); +}