@@ -142,16 +142,28 @@ std::vector<std::string> split_flags(std::string_view s) {
142142
143143namespace {
144144
145+ // The CDB's path contract: NATIVE separators, unconditionally. Every
146+ // ingestion point (manifest globs, include_dirs, build.mcpp directives) is
147+ // normalized at the source, but this is the LAST line — a path that slips
148+ // through with a mixed `root\a/b` spelling (MSVC keeps input `/` verbatim)
149+ // breaks CLion, and no amount of "all ingestion points are covered" can be
150+ // proven. make_preferred() is a no-op on POSIX.
151+ std::string native_string (const std::filesystem::path& p) {
152+ auto n = p;
153+ n.make_preferred ();
154+ return n.string ();
155+ }
156+
145157std::vector<std::string> local_include_args (const CompileUnit& cu) {
146158 std::vector<std::string> args;
147159 args.reserve (cu.localIncludeDirs .size ());
148160 for (auto const & inc : cu.localIncludeDirs ) {
149- args.push_back (" -I" + inc. string ( ));
161+ args.push_back (" -I" + native_string (inc ));
150162 }
151163 // #249: after-dirs keep their -idirafter spelling in the compile DB so
152164 // tooling (clangd) reproduces the compiler's search order.
153165 for (auto const & inc : cu.localIncludeDirsAfter ) {
154- args.push_back (" -idirafter" + inc. string ( ));
166+ args.push_back (" -idirafter" + native_string (inc ));
155167 }
156168 return args;
157169}
@@ -186,7 +198,7 @@ std::string emit_compile_commands(const BuildPlan& plan, const CompileFlags& fla
186198 : isCSource ? flags.cc
187199 : flags.cxx ;
188200
189- auto output_path = (plan.outputDir / cu.object ). string ( );
201+ auto output_path = native_string (plan.outputDir / cu.object );
190202
191203 // Build arguments array.
192204 nlohmann::json args = nlohmann::json::array ();
@@ -198,13 +210,13 @@ std::string emit_compile_commands(const BuildPlan& plan, const CompileFlags& fla
198210 for (auto & f : package_flag_args (cu, isCSource))
199211 args.push_back (std::move (f));
200212 args.push_back (" -c" );
201- args.push_back (cu.source . string ( ));
213+ args.push_back (native_string ( cu.source ));
202214 args.push_back (" -o" );
203215 args.push_back (output_path);
204216
205217 nlohmann::json entry;
206- entry[" directory" ] = plan.projectRoot . string ( );
207- entry[" file" ] = cu.source . string ( );
218+ entry[" directory" ] = native_string ( plan.projectRoot );
219+ entry[" file" ] = native_string ( cu.source );
208220 entry[" arguments" ] = std::move (args);
209221 entry[" output" ] = output_path;
210222
@@ -222,11 +234,25 @@ std::string merge_compile_commands(
222234 if (freshJ.is_discarded () || !freshJ.is_array ())
223235 return std::string (fresh);
224236
237+ // Dedup key = the file's PATH, spelled the way a fresh plan spells it
238+ // (native separators). A prior CDB written before the mixed-separator
239+ // fix (#390) carries `root\generated/modules\x.cppm` entries that are
240+ // the SAME file as the fresh `root\generated\modules\x.cppm` — a literal
241+ // string comparison would keep both and the user's upgrade would not
242+ // visibly fix anything. Normalizing makes the merge self-healing: the
243+ // stale mixed entry is skipped on the first `mcpp build` after upgrade.
244+ // fileExists still probes the raw spelling — Windows accepts both.
245+ auto norm_key = [](std::string_view f) {
246+ auto p = std::filesystem::path (std::string (f)).lexically_normal ();
247+ p.make_preferred ();
248+ return p.string ();
249+ };
250+
225251 // Files the current plan already covers — those entries are authoritative.
226252 std::set<std::string> freshFiles;
227253 for (auto const & e : freshJ) {
228254 if (e.contains (" file" ) && e[" file" ].is_string ())
229- freshFiles.insert (e[" file" ].get <std::string>());
255+ freshFiles.insert (norm_key ( e[" file" ].get <std::string>() ));
230256 }
231257
232258 // Keep fresh order, then append still-valid prior entries the plan doesn't
@@ -238,7 +264,7 @@ std::string merge_compile_commands(
238264 for (auto const & e : existingJ) {
239265 if (!e.contains (" file" ) || !e[" file" ].is_string ()) continue ;
240266 auto f = e[" file" ].get <std::string>();
241- if (freshFiles.contains (f)) continue ; // fresh wins
267+ if (freshFiles.contains (norm_key ( f))) continue ; // fresh wins
242268 if (!fileExists (std::filesystem::path (f))) continue ; // pruned
243269 merged.push_back (e);
244270 }
0 commit comments