Swift/Ninja: Fix emit-module-path clobbering on separate emit-module edge

Since commit b6367f723b (Swift/Ninja: Emit modules separately from
compilation, 2026-04-10) the compile edge still appends
-emit-module -emit-module-path when emitModuleSeparately is active,
duplicating the emit-module edge.  For targets with directory-style
module paths (CMP0195=NEW), this causes a file/directory conflict
that fails the build.

Guard -emit-module -emit-module-path on both the compile and
emit-module edges so they are not appended when the flags already
contain -emit-module-path (e.g. from the target's compile options).

Discovered building swift-syntax on Windows where the directory-style
path was overwritten by the flat path from the compile edge.

Issue: #27748
This commit is contained in:
Roman Lavrov
2026-04-27 18:57:47 -04:00
parent de52c08ba2
commit 899f1496f8
7 changed files with 113 additions and 9 deletions
+11 -6
View File
@@ -2200,8 +2200,8 @@ void cmNinjaTargetGenerator::WriteSwiftObjectBuildStatement(
objBuild.RspFile = cmStrCat(targetObjectFilename, ".swift.rsp");
// Importable targets keep -emit-module on compile so swiftc still emits
// .swiftdoc. When splitting module emission, only the .swiftmodule output
// moves to the separate emit-module edge.
// .swiftdoc. When splitting module emission, both the .swiftmodule output
// and -emit-module flags move entirely to the separate emit-module edge.
if (targetIsImportable && !emitModuleSeparately) {
objBuild.Outputs.push_back(moduleFilepath);
}
@@ -2240,7 +2240,8 @@ void cmNinjaTargetGenerator::WriteSwiftObjectBuildStatement(
std::string const moduleOutputPath =
this->LocalGenerator->ConvertToOutputFormat(moduleFilepath,
cmOutputConverter::SHELL);
if (targetIsImportable) {
if (targetIsImportable && !emitModuleSeparately &&
commonFlags.find("-emit-module-path") == std::string::npos) {
std::string const emitModuleFlag = "-emit-module";
std::string const modulePathFlag = "-emit-module-path";
this->LocalGenerator->AppendFlags(
@@ -2291,10 +2292,14 @@ void cmNinjaTargetGenerator::WriteSwiftObjectBuildStatement(
// Start from common flags (shared with compile edge) and add
// emit-module-specific flags. The emit-module rule template already
// contains -emit-module, so we only need -emit-module-path here.
// Skip if the flags already contain one (e.g. a directory-style path
// set by the target's compile options).
modBuild.Variables["FLAGS"] = commonFlags;
this->LocalGenerator->AppendFlags(
modBuild.Variables["FLAGS"],
cmStrCat("-emit-module-path ", moduleOutputPath));
if (commonFlags.find("-emit-module-path") == std::string::npos) {
this->LocalGenerator->AppendFlags(
modBuild.Variables["FLAGS"],
cmStrCat("-emit-module-path ", moduleOutputPath));
}
modBuild.RspFile = cmStrCat(moduleFilepath, ".rsp");
@@ -2,8 +2,17 @@ string(
REGEX MATCHALL
"swiftc(\\.exe)?\"? [^\n]* -emit-module-path [^\n]*L\\.swiftmodule"
swift_module_commands "${actual_stdout}")
list(LENGTH swift_module_commands swift_module_command_count)
if(swift_module_command_count LESS 2)
set(emit_module_commands "${swift_module_commands}")
list(FILTER emit_module_commands EXCLUDE REGEX " -c ")
if(NOT emit_module_commands)
string(APPEND RunCMake_TEST_FAILED
"Expected separate compile and emit-module commands for L, found ${swift_module_command_count} command(s) with '-emit-module-path ... L.swiftmodule'.\n")
"Expected an emit-module command with '-emit-module-path ... L.swiftmodule'.\n")
endif()
set(compile_commands "${swift_module_commands}")
list(FILTER compile_commands INCLUDE REGEX " -c ")
if(compile_commands)
string(APPEND RunCMake_TEST_FAILED
"Compile command (-c) should not contain '-emit-module-path' when emitting module separately.\n")
endif()
@@ -0,0 +1,26 @@
string(
REGEX MATCHALL
"swiftc(\\.exe)?\"? [^\n]* -emit-module-path [^\n]*L\\.swiftmodule[^ \n]*"
swift_module_commands "${actual_stdout}")
set(emit_module_commands "${swift_module_commands}")
list(FILTER emit_module_commands EXCLUDE REGEX " -c ")
if(NOT emit_module_commands)
string(APPEND RunCMake_TEST_FAILED
"Expected an emit-module command with '-emit-module-path ... L.swiftmodule'.\n")
endif()
set(compile_commands "${swift_module_commands}")
list(FILTER compile_commands INCLUDE REGEX " -c ")
if(compile_commands)
string(APPEND RunCMake_TEST_FAILED
"Compile command (-c) should not contain '-emit-module-path' when emitting module separately.\n")
endif()
# CMP0195=NEW: the emit-module path should be directory-style
# (L.swiftmodule/<triple>.swiftmodule), not flat.
list(FILTER emit_module_commands INCLUDE REGEX "L\\.swiftmodule(/|\\\\)[-_a-zA-Z0-9]+\\.swiftmodule")
if(NOT emit_module_commands)
string(APPEND RunCMake_TEST_FAILED
"Expected directory-style -emit-module-path (L.swiftmodule/<triple>.swiftmodule) from CMP0195=NEW.\n")
endif()
@@ -0,0 +1,15 @@
cmake_minimum_required(VERSION 4.0)
cmake_policy(SET CMP0157 NEW)
cmake_policy(SET CMP0195 NEW)
cmake_policy(SET CMP0215 NEW)
if(NOT CMAKE_GENERATOR MATCHES "Ninja")
message(SEND_ERROR "this test must use a Ninja generator, found ${CMAKE_GENERATOR}")
endif()
enable_language(Swift)
add_library(L STATIC L.swift)
add_library(LClient STATIC LClient.swift)
target_link_libraries(LClient PRIVATE L)
@@ -0,0 +1,11 @@
# When the target's flags already contain -emit-module-path, the emit-module
# edge should not append a second one that clobbers it.
# Match command lines that have two -emit-module-path flags on the same line.
string(
REGEX MATCHALL
"-emit-module-path [^\n]*-emit-module-path"
duplicate_flags "${actual_stdout}")
if(duplicate_flags)
string(APPEND RunCMake_TEST_FAILED
"Found command with duplicate -emit-module-path flags.\n")
endif()
@@ -0,0 +1,22 @@
cmake_minimum_required(VERSION 4.0)
cmake_policy(SET CMP0157 NEW)
cmake_policy(SET CMP0195 NEW)
cmake_policy(SET CMP0215 NEW)
if(NOT CMAKE_GENERATOR MATCHES "Ninja")
message(SEND_ERROR "this test must use a Ninja generator, found ${CMAKE_GENERATOR}")
endif()
enable_language(Swift)
add_library(L STATIC L.swift)
add_library(LClient STATIC LClient.swift)
target_link_libraries(LClient PRIVATE L)
# Simulate a target whose flags already contain -emit-module-path with a
# directory-style path (as seen with swift-syntax's CMake config). Use the
# real module triple so the path matches GetSwiftModulePath() and the build
# would succeed outside of dry-run.
target_compile_options(L PRIVATE
-emit-module-path ${CMAKE_CURRENT_BINARY_DIR}/L.swiftmodule/${CMAKE_Swift_MODULE_TRIPLE}.swiftmodule)
+16
View File
@@ -162,6 +162,22 @@ if(RunCMake_GENERATOR MATCHES "Ninja")
run_cmake_command(EmitModuleSeparatelyResponseFile-build ${CMAKE_COMMAND} --build . -- -vn)
endblock()
block()
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/EmitModuleSeparatelyDirectoryStyle-build)
run_cmake(EmitModuleSeparatelyDirectoryStyle)
set(RunCMake_TEST_NO_CLEAN 1)
# -v: verbose to capture executed commands -n: dry-run to avoid actually compiling
run_cmake_command(EmitModuleSeparatelyDirectoryStyle-build ${CMAKE_COMMAND} --build . -- -vn)
endblock()
block()
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/EmitModuleSeparatelyExistingModulePath-build)
run_cmake(EmitModuleSeparatelyExistingModulePath)
set(RunCMake_TEST_NO_CLEAN 1)
# -v: verbose to capture executed commands -n: dry-run to avoid actually compiling
run_cmake_command(EmitModuleSeparatelyExistingModulePath-build ${CMAKE_COMMAND} --build . -- -vn)
endblock()
block()
if(CMAKE_SYSTEM_NAME MATCHES Windows)
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/ImportLibraryFlags-build)