@@ -437,7 +437,15 @@ struct BuildPlan {
437437 // byte-for-byte unchanged; only a Windows prebuilt-DLL package (or a test
438438 // that ships a .dll) populates it. dest is relative to outputDir.
439439 struct DeployFile {
440- std::filesystem::path source; // absolute source DLL
440+ // Absolute source paths. Usually one; more than one means two or more
441+ // packages of this graph each generate a file for this destination
442+ // (SPEC-007 R4.2) — an `artifacts` dependency and its consumer asking
443+ // one plugin for the same translation catalog is the case #723 was
444+ // filed for. Planning no longer refuses this: at planning time a
445+ // generated source may not exist yet, so its content cannot be
446+ // compared. `mcpp stage` (mcpp.build.stage) is where the sources are
447+ // actually checked, at build time, against each other's bytes.
448+ std::vector<std::filesystem::path> sources;
441449 std::filesystem::path dest; // relative to outputDir, e.g. bin/libopenblas.dll
442450 };
443451 std::vector<DeployFile> runtimeDeployFiles;
@@ -1430,38 +1438,41 @@ make_plan(const mcpp::manifest::Manifest& manifest,
14301438
14311439 // `toDir` is a `runtime.deploy` destination, relative to the executable's
14321440 // directory; empty and "." both mean that directory itself, which is where
1433- // every `deploy_files` entry goes. The collision check keys on the full
1434- // relative destination, so two files of one name in two directories do not
1435- // collide, and two sources for one destination still do.
1441+ // every `deploy_files` entry goes. The check keys on the full relative
1442+ // destination, so two files of one name in two directories do not
1443+ // collide.
1444+ //
1445+ // Two sources for one destination are no longer refused HERE (SPEC-007
1446+ // R4.2, #723): at planning time a generated source may not exist yet, so
1447+ // its content cannot be compared. Both stay as inputs of the one
1448+ // `stage_file` edge this destination becomes (ninja_backend.cppm), and
1449+ // `mcpp stage` (mcpp.build.stage) is where the invariant — one
1450+ // destination, one content — is actually checked, once the sources exist.
1451+ // A source already listed for this destination (the ordinary case: the
1452+ // same file reached through two graph edges) is not duplicated.
14361453 auto add_deploy = [&](const std::filesystem::path& source,
1437- std::string_view toDir = {})
1438- -> std::optional<std::string> {
1454+ std::string_view toDir = {}) {
14391455 const auto normalized = source.lexically_normal ();
14401456 auto destDir = std::filesystem::path (" bin" );
14411457 if (!toDir.empty () && toDir != " ." ) destDir /= std::filesystem::path (toDir);
14421458 const auto dest = destDir / source.filename ();
14431459 auto existing = std::ranges::find_if (plan.runtimeDeployFiles ,
14441460 [&](auto const & value) { return value.dest == dest; });
14451461 if (existing != plan.runtimeDeployFiles .end ()) {
1446- if (existing->source .lexically_normal () != normalized) {
1447- return std::format (
1448- " runtime deploy collision: '{}' and '{}' both target '{}'" ,
1449- existing->source .string (), normalized.string (), dest.string ());
1450- }
1451- return std::nullopt ;
1462+ if (std::ranges::find (existing->sources , normalized)
1463+ == existing->sources .end ())
1464+ existing->sources .push_back (normalized);
1465+ return ;
14521466 }
1453- plan.runtimeDeployFiles .push_back ({normalized, dest});
1454- return std::nullopt ;
1467+ plan.runtimeDeployFiles .push_back ({{normalized}, dest});
14551468 };
14561469 // Structured deploy files are explicit and platform-neutral. Legacy
14571470 // library_dirs keeps its one-train DLL discovery behavior below.
14581471 for (auto const & source : plan.linkIntent .deployFiles ) {
1459- if (auto collision = add_deploy (source))
1460- return std::unexpected (std::move (*collision));
1472+ add_deploy (source);
14611473 }
14621474 for (auto const & entry : plan.linkIntent .deploy ) {
1463- if (auto collision = add_deploy (entry.from , entry.to ))
1464- return std::unexpected (std::move (*collision));
1475+ add_deploy (entry.from , entry.to );
14651476 }
14661477 for (auto const & dir : plan.linkIntent .runtimeSearchDirs ) {
14671478 std::error_code dirEc;
@@ -1472,8 +1483,7 @@ make_plan(const mcpp::manifest::Manifest& manifest,
14721483 std::ranges::transform (ext, ext.begin (),
14731484 [](unsigned char c){ return std::tolower (c); });
14741485 if (ext != " .dll" ) continue ;
1475- if (auto collision = add_deploy (entry.path ()))
1476- return std::unexpected (std::move (*collision));
1486+ add_deploy (entry.path ());
14771487 }
14781488 }
14791489 // The same private runtime directories embedded as executable RUNPATH are
0 commit comments