Skip to content

Commit 02df8c3

Browse files
committed
build: the response file is written in the grammar its driver reads
A response file is not one format. clang and GCC tokenize it the GNU way, where a backslash escapes the next character, so the Windows paths written plainly came back with their separators eaten: clang++: error: no such file or directory: 'D:amcpp-pluginsmcpp-pluginstestsall-rules-compiletarget.build-mcpp...' Each argument is therefore wrapped in single quotes for those drivers, inside which nothing is special, and an embedded single quote is closed, escaped and reopened; cl and clang-cl keep Windows quoting, where a backslash is literal. One case per grammar.
1 parent 89ed2cd commit 02df8c3

2 files changed

Lines changed: 47 additions & 15 deletions

File tree

‎src/build/build_program.cppm‎

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -426,9 +426,27 @@ struct BuildProgramEnv {
426426
// The suffix selects the fact: `DIR` (the payload directory), `SOURCE` and
427427
// `PROGRAM` (where it came from, mcpp#755), read back by `xpkg_source` and
428428
// `xpkg_program`.
429-
std::string response_file_body(std::span<const std::string> args) {
429+
std::string response_file_body(std::span<const std::string> args, bool gnuQuoting) {
430430
std::string body;
431431
for (auto const& a : args) {
432+
if (gnuQuoting) {
433+
// GNU TOKENIZATION TREATS A BACKSLASH AS AN ESCAPE, everywhere but
434+
// inside single quotes. A Windows path written plainly therefore
435+
// arrives with its separators eaten -- clang read
436+
// `D:\a\mcpp-plugins\...` back as `D:amcpp-plugins...` and
437+
// reported `no such file or directory`. Inside single quotes every
438+
// character is literal, so each argument is wrapped, and an embedded
439+
// single quote is closed, escaped and reopened.
440+
body.push_back('\'');
441+
for (char c : a) {
442+
if (c == '\'') body += "'\\''";
443+
else body.push_back(c);
444+
}
445+
body += "'\n";
446+
continue;
447+
}
448+
// Windows tokenization (cl, clang-cl): a backslash is literal except
449+
// before a quote, so only whitespace and quotes need handling.
432450
if (a.find_first_of(" \t\"") == std::string::npos) {
433451
body += a;
434452
body.push_back('\n');
@@ -465,16 +483,17 @@ inline std::string xpkg_env_var(std::string_view ns, std::string_view name,
465483
return out;
466484
}
467485

468-
// THE ARGV THAT PASSES `args` THROUGH A RESPONSE FILE, and the file's content.
486+
// THE CONTENT OF A RESPONSE FILE CARRYING `args`, one argument per line.
469487
//
470-
// Every compiler driver mcpp supports reads `@file` with one argument per line.
471-
// An argument that carries whitespace or a quote is quoted, and the backslashes
472-
// before a quote -- including a run that would otherwise escape the closing one
473-
// -- are escaped, which is what makes a Windows path safe to write here.
488+
// Every compiler driver mcpp supports reads `@file`, but not with one grammar:
489+
// clang and GCC tokenize it the GNU way, where a backslash escapes the next
490+
// character, and cl and clang-cl tokenize it the Windows way, where a backslash
491+
// is literal. `gnuQuoting` picks between them -- single quotes, inside which
492+
// nothing is special, or Windows quoting of whitespace and quotes.
474493
//
475494
// Exported because its quoting is the part worth testing, and the command it
476495
// serves cannot be run on a host whose limit it does not cross.
477-
std::string response_file_body(std::span<const std::string> args);
496+
std::string response_file_body(std::span<const std::string> args, bool gnuQuoting);
478497

479498
// Does a compiler's output say the program asked for something the bundled
480499
// `mcpp` module does not have?
@@ -1914,7 +1933,7 @@ std::expected<void, std::string> run_build_program_impl(
19141933
flat, mcpp::platform::is_windows, /*needsShell=*/true)) {
19151934
const auto rsp = bdir / "build.mcpp.compile.rsp";
19161935
const auto body = response_file_body(
1917-
std::span<const std::string>(compileArgv).subspan(1));
1936+
std::span<const std::string>(compileArgv).subspan(1), !msvcHost);
19181937
std::ofstream out(rsp, std::ios::binary | std::ios::trunc);
19191938
out << body;
19201939
out.close();

‎tests/unit/test_sources.cpp‎

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -235,27 +235,40 @@ TEST(Sources, OverrideVersionIsCheckedAgainstRequirements) {
235235

236236
// ── The response file a long compile command goes through ───────────────────
237237

238-
TEST(Sources, ResponseFileWritesOneArgumentPerLineAndQuotesWhatNeedsIt) {
238+
TEST(Sources, ResponseFileQuotesForTheWindowsTokenizer) {
239239
const std::vector<std::string> args{
240240
"-std=c++23",
241-
"-fmodule-file=mcpp=/home/u/p/mcpp.pcm",
242241
"/Tp C:/Program Files/x/build.mcpp",
243242
R"(-DNAME="v")",
244243
"C:\\with space\\dir\\",
245244
};
246-
const auto body = mcpp::build::response_file_body(args);
247-
EXPECT_EQ(std::ranges::count(body, '\n'), 5);
245+
const auto body = mcpp::build::response_file_body(args, /*gnuQuoting=*/false);
246+
EXPECT_EQ(std::ranges::count(body, '\n'), 4);
248247
EXPECT_NE(body.find("-std=c++23\n"), std::string::npos) << body;
249-
// Quoted only where it has to be.
250-
EXPECT_NE(body.find("-fmodule-file=mcpp=/home/u/p/mcpp.pcm\n"), std::string::npos) << body;
251248
EXPECT_NE(body.find("\"/Tp C:/Program Files/x/build.mcpp\"\n"), std::string::npos) << body;
252-
// A quote inside the argument survives as one.
253249
EXPECT_NE(body.find("\"-DNAME=\\\"v\\\"\"\n"), std::string::npos) << body;
254250
// The run of backslashes that ends the argument is doubled, so it does not
255251
// escape the closing quote.
256252
EXPECT_NE(body.find("\"C:\\with space\\dir\\\\\"\n"), std::string::npos) << body;
257253
}
258254

255+
TEST(Sources, ResponseFileKeepsBackslashesLiteralForTheGnuTokenizer) {
256+
// clang and GCC read a backslash as an escape, so a Windows path written
257+
// plainly comes back with its separators eaten; inside single quotes
258+
// nothing is special.
259+
const std::vector<std::string> args{
260+
"-fmodule-file=mcpp=D:\\a\\p\\mcpp.pcm",
261+
"D:\\a\\obj\\x.o",
262+
"-DNAME=it's",
263+
};
264+
const auto body = mcpp::build::response_file_body(args, /*gnuQuoting=*/true);
265+
EXPECT_EQ(std::ranges::count(body, '\n'), 3);
266+
EXPECT_NE(body.find("'-fmodule-file=mcpp=D:\\a\\p\\mcpp.pcm'\n"), std::string::npos) << body;
267+
EXPECT_NE(body.find("'D:\\a\\obj\\x.o'\n"), std::string::npos) << body;
268+
// A single quote in the argument closes, escapes and reopens.
269+
EXPECT_NE(body.find("'-DNAME=it'\\''s'\n"), std::string::npos) << body;
270+
}
271+
259272
// ── [xlings.overrides] in config.toml ───────────────────────────────────────
260273

261274
TEST(Sources, ConfigOverridesParseWithTheManifestsTwoShapes) {

0 commit comments

Comments
 (0)