Repository: mesonbuild/meson

Issue:
unparsable ninja.build generated from run_command with a multi-line string
```meson
project('meson7' ,'c')
run_target(
  'update-rules',
  command : ['sh', '-c',
	     '
python3 make-man-rules.py --meson'])
```

```
ninja: error: build.ninja:64: expected '=', got lexing error
python3$ make-man-rules.py$ --meson'
       ^ near here
WARNING: Could not create compilation database.
```


Discussion:

Resolving patch:
diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py
--- a/mesonbuild/backend/ninjabackend.py
+++ b/mesonbuild/backend/ninjabackend.py
@@ -38,7 +38,12 @@
     rmfile_prefix = 'rm -f {} &&'
 
 def ninja_quote(text):
-    return text.replace(' ', '$ ').replace(':', '$:')
+    for char in ('$', ' ', ':'):
+        text = text.replace(char, '$' + char)
+    if '\n' in text:
+        raise MesonException('Ninja does not support newlines in rules. '
+                             'Please report this error with a test case to the Meson bug tracker.')
+    return text
 
 
 class NinjaBuildElement:
@@ -480,12 +485,16 @@ def generate_custom_target(self, target, outfile):
         # If the target requires capturing stdout, then use the serialized
         # executable wrapper to capture that output and save it to a file.
         #
+        # If the command line requires a newline, also use the wrapper, as
+        # ninja does not support them in its build rule syntax.
+        #
         # Windows doesn't have -rpath, so for EXEs that need DLLs built within
         # the project, we need to set PATH so the DLLs are found. We use
         # a serialized executable wrapper for that and check if the
         # CustomTarget command needs extra paths first.
-        if target.capture or ((mesonlib.is_windows() or mesonlib.is_cygwin()) and
-                              self.determine_windows_extra_paths(target.command[0])):
+        if (target.capture or any('\n' in c for c in cmd) or
+                ((mesonlib.is_windows() or mesonlib.is_cygwin()) and
+                 self.determine_windows_extra_paths(target.command[0]))):
             exe_data = self.serialize_executable(target.command[0], cmd[1:],
                                                  # All targets are built from the build dir
                                                  self.environment.get_build_dir(),
@@ -1448,12 +1457,13 @@ def generate_rust_compile_rules(self, compiler, outfile):
 
     def generate_swift_compile_rules(self, compiler, outfile):
         rule = 'rule %s_COMPILER\n' % compiler.get_language()
-        full_exe = [sys.executable,
-                    self.environment.get_build_command(),
+        full_exe = [ninja_quote(sys.executable),
+                    ninja_quote(self.environment.get_build_command()),
                     '--internal',
                     'dirchanger',
-                    '$RUNDIR'] + compiler.get_exelist()
-        invoc = ' '.join([ninja_quote(i) for i in full_exe])
+                    '$RUNDIR']
+        invoc = (' '.join(full_exe) + ' ' +
+                 ' '.join(ninja_quote(i) for i in compiler.get_exelist()))
         command = ' command = %s $ARGS $in\n' % invoc
         description = ' description = Compiling Swift source $in.\n'
         outfile.write(rule)


Test patch:
diff --git a/test cases/common/149 special characters/check_quoting.py b/test cases/common/149 special characters/check_quoting.py
new file mode 100644
--- /dev/null
+++ b/test cases/common/149 special characters/check_quoting.py	
@@ -0,0 +1,28 @@
+#!/usr/bin/env python3
+
+import sys
+
+expected = {
+    'newline': '\n',
+    'dollar': '$',
+    'colon': ':',
+    'space': ' ',
+    'multi1': '  ::$$  ::$$',
+    'multi2': '  ::$$\n\n  \n\n::$$',
+}
+
+output = None
+
+for arg in sys.argv[1:]:
+    try:
+        name, value = arg.split('=', 1)
+    except ValueError:
+        output = arg
+        continue
+
+    if expected[name] != value:
+        raise RuntimeError('{!r} is {!r} but should be {!r}'.format(name, value, expected[name]))
+
+if output is not None:
+    with open(output, 'w') as f:
+        f.write('Success!')
diff --git a/test cases/common/149 special characters/installed_files.txt b/test cases/common/149 special characters/installed_files.txt
new file mode 100644
--- /dev/null
+++ b/test cases/common/149 special characters/installed_files.txt	
@@ -0,0 +1,2 @@
+usr/share/result
+usr/share/result2
diff --git a/test cases/common/149 special characters/meson.build b/test cases/common/149 special characters/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/common/149 special characters/meson.build	
@@ -0,0 +1,37 @@
+project('ninja special characters' ,'c')
+
+python = import('python3').find_python()
+
+# Without newlines, this should appear directly in build.ninja.
+gen = custom_target('gen',
+  command : [
+    python,
+    files('check_quoting.py'),
+    'dollar=$',
+    'colon=:',
+    'space= ',
+    '''multi1=  ::$$  ::$$''',
+    '@OUTPUT@'],
+  output : 'result',
+  install : true,
+  install_dir : get_option('datadir'))
+
+# With newlines, this should go through the exe wrapper.
+gen2 = custom_target('gen2',
+  command : [
+    python,
+    files('check_quoting.py'),
+    '''newline=
+''',
+    'dollar=$',
+    'colon=:',
+    'space= ',
+    '''multi2=  ::$$
+
+  
+
+::$$''',
+    '@OUTPUT@'],
+  output : 'result2',
+  install : true,
+  install_dir : get_option('datadir'))