cmCommonTargetGenerator: Improve launcher shell conversion

Encapsulate the shell escaping logic in `cmCommonTargetGenerator` and
represent launchers via a vector of strings (as is done with custom
commands, execute process commands, etc.) to make them easier to work
with.

This facilitates work in a future commit which will reuse this pathway.

Issue: #27598, #27402
This commit is contained in:
Tyler Yankee
2026-09-21 13:45:54 -04:00
parent bf1a2de409
commit 721eb8a5a2
18 changed files with 183 additions and 61 deletions
+37 -20
View File
@@ -8,6 +8,7 @@
#include <cm/filesystem> #include <cm/filesystem>
#include <cm/string_view> #include <cm/string_view>
#include <cmext/algorithm>
#include <cmext/string_view> #include <cmext/string_view>
#include "cmComputeLinkInformation.h" #include "cmComputeLinkInformation.h"
@@ -422,10 +423,10 @@ void cmCommonTargetGenerator::AppendOSXVerFlag(std::string& flags,
} }
} }
std::string cmCommonTargetGenerator::GetCompilerLauncher( std::vector<std::string> cmCommonTargetGenerator::GetCompilerLauncher(
std::string const& lang, std::string const& config) std::string const& lang, std::string const& config)
{ {
std::string compilerLauncher; std::vector<std::string> compilerLauncher;
if (lang == "C" || lang == "CXX" || lang == "Fortran" || lang == "CUDA" || if (lang == "C" || lang == "CXX" || lang == "Fortran" || lang == "CUDA" ||
lang == "HIP" || lang == "ISPC" || lang == "OBJC" || lang == "OBJCXX") { lang == "HIP" || lang == "ISPC" || lang == "OBJC" || lang == "OBJCXX") {
std::string const propName = cmStrCat(lang, "_COMPILER_LAUNCHER"); std::string const propName = cmStrCat(lang, "_COMPILER_LAUNCHER");
@@ -434,14 +435,15 @@ std::string cmCommonTargetGenerator::GetCompilerLauncher(
*cLauncherValue, this->GeneratorTarget->GetLocalGenerator(), config, *cLauncherValue, this->GeneratorTarget->GetLocalGenerator(), config,
this->GeneratorTarget, nullptr, this->GeneratorTarget, lang); this->GeneratorTarget, nullptr, this->GeneratorTarget, lang);
if (!evaluatedCLauncher.empty()) { if (!evaluatedCLauncher.empty()) {
compilerLauncher = evaluatedCLauncher; cm::append(compilerLauncher,
cmList{ evaluatedCLauncher, cmList::EmptyElements::Yes });
} }
} }
return compilerLauncher; return compilerLauncher;
} }
std::string cmCommonTargetGenerator::GenerateCodeCheckRules( std::string cmCommonTargetGenerator::GenerateCodeCheckRules(
cmSourceFile const& source, std::string& compilerLauncher, cmSourceFile const& source, std::vector<std::string>& compilerLauncher,
std::string const& cmakeCmd, std::string const& config, std::string const& cmakeCmd, std::string const& config,
std::function<std::string(std::string const&)> const& pathConverter) std::function<std::string(std::string const&)> const& pathConverter)
{ {
@@ -488,11 +490,14 @@ std::string cmCommonTargetGenerator::GenerateCodeCheckRules(
std::string code_check = cmakeCmd + " -E __run_co_compile"; std::string code_check = cmakeCmd + " -E __run_co_compile";
if (!compilerLauncher.empty()) { if (!compilerLauncher.empty()) {
// In __run_co_compile case the launcher command is supplied // In __run_co_compile case the launcher command is supplied
// via --launcher=<maybe-list> and consumed // via --launcher=<list> and consumed
for (std::string& arg : compilerLauncher) {
cmSystemTools::ReplaceString(arg, ";", "\\;");
}
code_check = code_check =
cmStrCat(std::move(code_check), " --launcher=", cmStrCat(std::move(code_check), " --launcher=",
this->GeneratorTarget->GetLocalGenerator()->EscapeForShell( this->GeneratorTarget->GetLocalGenerator()->EscapeForShell(
compilerLauncher)); cmJoin(compilerLauncher, ";")));
compilerLauncher.clear(); compilerLauncher.clear();
} }
if (cmNonempty(iwyu)) { if (cmNonempty(iwyu)) {
@@ -639,32 +644,44 @@ std::string cmCommonTargetGenerator::GenerateCodeCheckRules(
return ""; return "";
} }
std::string cmCommonTargetGenerator::GetLinkerLauncher( std::vector<std::string> cmCommonTargetGenerator::GetLinkerLauncher(
std::string const& config) std::string const& config)
{ {
std::string lang = this->GeneratorTarget->GetLinkerLanguage(config); std::vector<std::string> linkLauncher;
std::string propName = lang + "_LINKER_LAUNCHER"; std::string const lang = this->GeneratorTarget->GetLinkerLanguage(config);
std::string const propName = cmStrCat(lang, "_LINKER_LAUNCHER");
cmValue launcherProp = this->GeneratorTarget->GetProperty(propName); cmValue launcherProp = this->GeneratorTarget->GetProperty(propName);
if (cmNonempty(launcherProp)) { if (cmNonempty(launcherProp)) {
cm::GenEx::Context context(this->LocalCommonGenerator, config, lang); cm::GenEx::Context context(this->LocalCommonGenerator, config, lang);
cmGeneratorExpressionDAGChecker dagChecker{ this->GeneratorTarget, cmGeneratorExpressionDAGChecker dagChecker{ this->GeneratorTarget,
propName, nullptr, nullptr, propName, nullptr, nullptr,
context }; context };
std::string evaluatedLinklauncher = cmGeneratorExpression::Evaluate(
std::string const evaluatedLinkLauncher = cmGeneratorExpression::Evaluate(
*launcherProp, context.LG, context.Config, this->GeneratorTarget, *launcherProp, context.LG, context.Config, this->GeneratorTarget,
&dagChecker, this->GeneratorTarget, context.Language); &dagChecker, this->GeneratorTarget, context.Language);
// Convert ;-delimited list to single string if (!evaluatedLinkLauncher.empty()) {
cmList args{ evaluatedLinklauncher, cmList::EmptyElements::Yes }; cm::append(linkLauncher,
if (!args.empty()) { cmList{ evaluatedLinkLauncher, cmList::EmptyElements::Yes });
args[0] = this->LocalCommonGenerator->ConvertToOutputFormat(
args[0], cmOutputConverter::SHELL);
for (std::string& i : cmMakeRange(args.begin() + 1, args.end())) {
i = this->LocalCommonGenerator->EscapeForShell(i);
}
return cmJoin(args, " ");
} }
} }
return std::string(); return linkLauncher;
}
std::string cmCommonTargetGenerator::ConvertLauncherToShell(
std::vector<std::string> const& launcher) const
{
if (launcher.empty()) {
return std::string{};
}
std::vector<std::string> args = launcher;
args[0] = this->LocalCommonGenerator->ConvertToOutputFormat(
args[0], cmOutputConverter::SHELL);
for (std::string& arg : cmMakeRange(args.begin() + 1, args.end())) {
arg = this->LocalCommonGenerator->EscapeForShell(arg);
}
return cmJoin(args, " ");
} }
bool cmCommonTargetGenerator::HaveRequiredLanguages( bool cmCommonTargetGenerator::HaveRequiredLanguages(
+7 -4
View File
@@ -68,12 +68,12 @@ protected:
std::string GetManifests(std::string const& config); std::string GetManifests(std::string const& config);
std::string GetAIXExports(std::string const& config); std::string GetAIXExports(std::string const& config);
std::string GenerateCodeCheckRules( std::string GenerateCodeCheckRules(
cmSourceFile const& source, std::string& compilerLauncher, cmSourceFile const& source, std::vector<std::string>& compilerLauncher,
std::string const& cmakeCmd, std::string const& config, std::string const& cmakeCmd, std::string const& config,
std::function<std::string(std::string const&)> const& pathConverter); std::function<std::string(std::string const&)> const& pathConverter);
std::string GetCompilerLauncher(std::string const& lang, std::vector<std::string> GetCompilerLauncher(std::string const& lang,
std::string const& config); std::string const& config);
struct LinkedTargetDirs struct LinkedTargetDirs
{ {
@@ -85,7 +85,10 @@ protected:
std::string const& config) const; std::string const& config) const;
std::string ComputeTargetCompilePDB(std::string const& config) const; std::string ComputeTargetCompilePDB(std::string const& config) const;
std::string GetLinkerLauncher(std::string const& config); std::vector<std::string> GetLinkerLauncher(std::string const& config);
std::string ConvertLauncherToShell(
std::vector<std::string> const& launcher) const;
bool HaveRequiredLanguages(std::vector<cmSourceFile const*> const& sources, bool HaveRequiredLanguages(std::vector<cmSourceFile const*> const& sources,
std::set<std::string>& languagesNeeded) const; std::set<std::string>& languagesNeeded) const;
+7 -12
View File
@@ -822,11 +822,12 @@ void cmFastbuildNormalTargetGenerator::AddCompilerLaunchersForLanguages()
this->GetGeneratorTarget(), "RULE_LAUNCH_COMPILE", Config); this->GetGeneratorTarget(), "RULE_LAUNCH_COMPILE", Config);
// See if we need to use a compiler launcher like ccache or distcc // See if we need to use a compiler launcher like ccache or distcc
for (std::string const& language : Languages) { for (std::string const& language : Languages) {
std::string const compilerLauncher = std::vector<std::string> expanded =
cmCommonTargetGenerator::GetCompilerLauncher(language, Config); cmCommonTargetGenerator::GetCompilerLauncher(language, Config);
LogMessage("compilerLauncher: " + compilerLauncher); LogMessage("compilerLauncher: " + cmJoin(expanded, ";"));
std::vector<std::string> expanded; // FIXME(#27402): Empty arguments are not supported here.
cmExpandList(compilerLauncher, expanded); expanded.erase(std::remove(expanded.begin(), expanded.end(), ""),
expanded.end());
if (!expanded.empty()) { if (!expanded.empty()) {
std::string const exe = expanded[0]; std::string const exe = expanded[0];
@@ -844,14 +845,8 @@ void cmFastbuildNormalTargetGenerator::AddCompilerLaunchersForLanguages()
} }
void cmFastbuildNormalTargetGenerator::AddLinkerLauncher() void cmFastbuildNormalTargetGenerator::AddLinkerLauncher()
{ {
std::string const linkerLauncher = std::vector<std::string> args =
cmCommonTargetGenerator::GetLinkerLauncher(Config); cmCommonTargetGenerator::GetLinkerLauncher(Config);
std::vector<std::string> args;
#ifdef _WIN32
cmSystemTools::ParseWindowsCommandLine(linkerLauncher.c_str(), args);
#else
cmSystemTools::ParseUnixCommandLine(linkerLauncher.c_str(), args);
#endif
if (!args.empty()) { if (!args.empty()) {
std::string const exe = std::move(args[0]); std::string const exe = std::move(args[0]);
args.erase(args.begin()); args.erase(args.begin());
@@ -1191,7 +1186,7 @@ std::string cmFastbuildNormalTargetGenerator::ComputeCodeCheckOptions(
if (skipCodeCheck) { if (skipCodeCheck) {
return {}; return {};
} }
std::string compilerLauncher; std::vector<std::string> compilerLauncher;
std::string staticCheckRule = this->GenerateCodeCheckRules( std::string staticCheckRule = this->GenerateCodeCheckRules(
srcFile, compilerLauncher, "", Config, nullptr); srcFile, compilerLauncher, "", Config, nullptr);
LogMessage(cmStrCat("CodeCheck: ", staticCheckRule)); LogMessage(cmStrCat("CodeCheck: ", staticCheckRule));
@@ -592,8 +592,8 @@ void cmMakefileExecutableTargetGenerator::WriteExecutableRule(bool relink)
vars.LinkFlags = linkFlags.c_str(); vars.LinkFlags = linkFlags.c_str();
vars.Manifests = manifests.c_str(); vars.Manifests = manifests.c_str();
std::string linkerLauncher = std::string const linkerLauncher = this->ConvertLauncherToShell(
this->GetLinkerLauncher(this->GetConfigName()); this->GetLinkerLauncher(this->GetConfigName()));
if (cmNonempty(linkerLauncher)) { if (cmNonempty(linkerLauncher)) {
vars.Launcher = linkerLauncher.c_str(); vars.Launcher = linkerLauncher.c_str();
} }
+2 -2
View File
@@ -876,8 +876,8 @@ void cmMakefileLibraryTargetGenerator::WriteLibraryRules(
vars.LanguageCompileFlags = langFlags.c_str(); vars.LanguageCompileFlags = langFlags.c_str();
std::string linkerLauncher = std::string const linkerLauncher = this->ConvertLauncherToShell(
this->GetLinkerLauncher(this->GetConfigName()); this->GetLinkerLauncher(this->GetConfigName()));
if (cmNonempty(linkerLauncher)) { if (cmNonempty(linkerLauncher)) {
vars.Launcher = linkerLauncher.c_str(); vars.Launcher = linkerLauncher.c_str();
} }
+3 -10
View File
@@ -1106,7 +1106,7 @@ void cmMakefileTargetGenerator::WriteObjectRuleFiles(
} }
// See if we need to use a compiler launcher like ccache or distcc // See if we need to use a compiler launcher like ccache or distcc
std::string compilerLauncher; std::vector<std::string> compilerLauncher;
if (!compileCommands.empty()) { if (!compileCommands.empty()) {
compilerLauncher = GetCompilerLauncher(lang, config); compilerLauncher = GetCompilerLauncher(lang, config);
} }
@@ -1131,15 +1131,8 @@ void cmMakefileTargetGenerator::WriteObjectRuleFiles(
// If compiler launcher was specified and not consumed above, it // If compiler launcher was specified and not consumed above, it
// goes to the beginning of the command line. // goes to the beginning of the command line.
if (!compileCommands.empty() && !compilerLauncher.empty()) { if (!compileCommands.empty() && !compilerLauncher.empty()) {
cmList args{ compilerLauncher, cmList::EmptyElements::Yes }; compileCommands.front().insert(
if (!args.empty()) { 0, cmStrCat(this->ConvertLauncherToShell(compilerLauncher), ' '));
args[0] = this->LocalGenerator->ConvertToOutputFormat(
args[0], cmOutputConverter::SHELL);
for (std::string& i : cmMakeRange(args.begin() + 1, args.end())) {
i = this->LocalGenerator->EscapeForShell(i);
}
}
compileCommands.front().insert(0, args.join(" ") + " ");
} }
std::string launcher; std::string launcher;
+2 -1
View File
@@ -584,7 +584,8 @@ void cmNinjaNormalTargetGenerator::WriteLinkRule(
vars.LanguageCompileFlags = langFlags.c_str(); vars.LanguageCompileFlags = langFlags.c_str();
} }
std::string linkerLauncher = this->GetLinkerLauncher(config); std::string const linkerLauncher =
this->ConvertLauncherToShell(this->GetLinkerLauncher(config));
if (cmNonempty(linkerLauncher)) { if (cmNonempty(linkerLauncher)) {
vars.Launcher = linkerLauncher.c_str(); vars.Launcher = linkerLauncher.c_str();
} }
+2 -10
View File
@@ -47,7 +47,6 @@
#include "cmNinjaUtilityTargetGenerator.h" #include "cmNinjaUtilityTargetGenerator.h"
#include "cmOutputConverter.h" #include "cmOutputConverter.h"
#include "cmPolicies.h" #include "cmPolicies.h"
#include "cmRange.h"
#include "cmRulePlaceholderExpander.h" #include "cmRulePlaceholderExpander.h"
#include "cmSourceFile.h" #include "cmSourceFile.h"
#include "cmSourceFileLocationKind.h" #include "cmSourceFileLocationKind.h"
@@ -1598,15 +1597,8 @@ void cmNinjaTargetGenerator::WriteObjectBuildStatement(
// If compiler launcher was specified and not consumed above, it // If compiler launcher was specified and not consumed above, it
// goes to the beginning of the command line. // goes to the beginning of the command line.
if (!compilerLauncher.empty()) { if (!compilerLauncher.empty()) {
cmList args{ compilerLauncher, cmList::EmptyElements::Yes }; vars["LAUNCHER"] =
if (!args.empty()) { cmStrCat(this->ConvertLauncherToShell(compilerLauncher), ' ');
args[0] = this->LocalGenerator->ConvertToOutputFormat(
args[0], cmOutputConverter::SHELL);
for (std::string& i : cmMakeRange(args.begin() + 1, args.end())) {
i = this->LocalGenerator->EscapeForShell(i);
}
vars["LAUNCHER"] = args.join(" ") + " ";
}
} }
if (this->GetMakefile()->GetSafeDefinition( if (this->GetMakefile()->GetSafeDefinition(
@@ -0,0 +1 @@
special-args launcher ok: compile
@@ -0,0 +1 @@
special-args launcher ok: compile
@@ -0,0 +1,3 @@
# Lint tools run the launcher through "cmake -E __run_co_compile --launcher=...".
set(CMAKE_C_CPPCHECK "${CMAKE_COMMAND};-E;true")
include(C-special-args.cmake)
@@ -0,0 +1,4 @@
set(check "${CMAKE_CURRENT_LIST_DIR}/check-special-args.cmake")
set(CMAKE_C_COMPILER_LAUNCHER
"${CMAKE_COMMAND};-P;${check};--;compile;;semi\\;colon;with space")
include(C-common.cmake)
@@ -51,3 +51,10 @@ foreach(lang ${langs})
run_compiler_launcher_env(${lang}-launch-env) run_compiler_launcher_env(${lang}-launch-env)
endif() endif()
endforeach() endforeach()
if(NOT RunCMake_GENERATOR MATCHES "FASTBuild")
# FIXME(#27402): FASTBuild builds its launcher command line without shell
# quoting, so it can't handle empty or special-character arguments.
run_compiler_launcher(C-special-args)
run_compiler_launcher(C-special-args-cppcheck)
endif()
@@ -0,0 +1,41 @@
# Verifies that the launcher's own arguments arrive intact, then runs the
# launched command.
set(base 4) # CMAKE_ARGV0..3 are: cmake -P <script> --
set(num_args 4) # <phase> plus the three arguments below
set(expect_1 "")
set(expect_2 "semi;colon")
set(expect_3 "with space")
math(EXPR min_argc "${base} + ${num_args} + 1")
if(CMAKE_ARGC LESS min_argc)
message(FATAL_ERROR "too few arguments: ${CMAKE_ARGC}")
endif()
foreach(i RANGE 1 3)
math(EXPR idx "${base} + ${i}")
if(NOT "${CMAKE_ARGV${idx}}" STREQUAL "${expect_${i}}")
message(FATAL_ERROR
"launcher argument ${i}: expected [${expect_${i}}] but got [${CMAKE_ARGV${idx}}]")
endif()
endforeach()
message("special-args launcher ok: ${CMAKE_ARGV${base}}")
# Build and invoke the launched command via cmake_language(EVAL CODE) using
# bracket arguments, rather than a ";"-separated list. A list element ending
# in a backslash (e.g. MSVC's "/Fd<dir>\") would otherwise merge with the
# list's own separator when the list is later expanded, corrupting the
# command line.
set(cmd_code "execute_process(COMMAND")
math(EXPR first "${base} + ${num_args}")
math(EXPR last "${CMAKE_ARGC} - 1")
foreach(i RANGE ${first} ${last})
string(APPEND cmd_code " [==[${CMAKE_ARGV${i}}]==]")
endforeach()
string(APPEND cmd_code " RESULT_VARIABLE result)")
cmake_language(EVAL CODE "${cmd_code}")
if(NOT result EQUAL 0)
message(FATAL_ERROR "launched command failed: ${result}")
endif()
@@ -0,0 +1,12 @@
# Launcher invocations cannot be reliably verified via stdout, so it records to
# a log file instead. Verify by checking its content.
set(log_file "${RunCMake_TEST_BINARY_DIR}/special-args.log")
if(NOT EXISTS "${log_file}")
set(RunCMake_TEST_FAILED "special-args.log was not created by the wrapper")
else()
file(READ "${log_file}" log_content)
if(NOT log_content MATCHES "link")
set(RunCMake_TEST_FAILED
"special-args.log does not show link launcher ran:\n${log_content}")
endif()
endif()
@@ -0,0 +1,4 @@
set(check "${CMAKE_CURRENT_LIST_DIR}/check-special-args.cmake")
set(CMAKE_C_LINKER_LAUNCHER
"${CMAKE_COMMAND};-P;${check};--;link;;semi\\;colon;with space")
include(C-common.cmake)
@@ -48,3 +48,9 @@ foreach(lang ${langs})
run_linker_launcher_env(${lang}-launch-env) run_linker_launcher_env(${lang}-launch-env)
endif() endif()
endforeach() endforeach()
if(NOT RunCMake_GENERATOR MATCHES "FASTBuild")
# FIXME(#27402): FASTBuild builds its launcher command line without shell
# quoting, so it can't handle empty or special-character arguments.
run_linker_launcher(C-special-args)
endif()
@@ -0,0 +1,42 @@
# Verifies that the launcher's own arguments arrive intact, then runs the
# launched command.
set(base 4) # CMAKE_ARGV0..3 are: cmake -P <script> --
set(num_args 4) # <phase> plus the three arguments below
set(expect_1 "")
set(expect_2 "semi;colon")
set(expect_3 "with space")
math(EXPR min_argc "${base} + ${num_args} + 1")
if(CMAKE_ARGC LESS min_argc)
message(FATAL_ERROR "too few arguments: ${CMAKE_ARGC}")
endif()
foreach(i RANGE 1 3)
math(EXPR idx "${base} + ${i}")
if(NOT "${CMAKE_ARGV${idx}}" STREQUAL "${expect_${i}}")
message(FATAL_ERROR
"launcher argument ${i}: expected [${expect_${i}}] but got [${CMAKE_ARGV${idx}}]")
endif()
endforeach()
file(APPEND "special-args.log" "${CMAKE_ARGV${base}}\n")
# Build and invoke the launched command via cmake_language(EVAL CODE) using
# bracket arguments, rather than a ";"-separated list. A list element ending
# in a backslash (e.g. MSVC's "/Fd<dir>\") would otherwise merge with the
# list's own separator when the list is later expanded, corrupting the
# command line.
set(cmd_code "execute_process(COMMAND")
math(EXPR first "${base} + ${num_args}")
math(EXPR last "${CMAKE_ARGC} - 1")
foreach(i RANGE ${first} ${last})
string(APPEND cmd_code " [==[${CMAKE_ARGV${i}}]==]")
endforeach()
string(APPEND cmd_code " RESULT_VARIABLE result)")
cmake_language(EVAL CODE "${cmd_code}")
if(NOT result EQUAL 0)
message(FATAL_ERROR "launched command failed: ${result}")
endif()