Repository: conan-io/conan

Issue:
Escaping espaces for defines in cmake generator
Comes from: https://github.com/conan-io/cmake-conan/issues/82

```python
    def package_info(self):
        self.cpp_info.defines= ['MY_DEF=My string', 'MY_DEF2=My other string']
```

Fails because of the spaces. Escaping in cmake works.

cc/ @atvise


Discussion:

Resolving patch:
diff --git a/conans/client/generators/cmake.py b/conans/client/generators/cmake.py
--- a/conans/client/generators/cmake.py
+++ b/conans/client/generators/cmake.py
@@ -7,42 +7,46 @@
 
 class DepsCppCmake(object):
     def __init__(self, cpp_info):
-        def escape(value, path=False):
-            """Escapes the specified string for use in a CMake command surrounded with double quotes
-                :param value the string to escape
-                :param path True if this should be treated as a path (backslash will be transformed to forward slash)
-                                 or False otherwise (backslash will be transformed to double backslash)"""
-            return '{0}'.format(value.replace('\\', '/' if path else '\\\\')
-                                     .replace('$', '\\$')
-                                     .replace('"', '\\"'))
-
-        def multiline(field):
-            return "\n\t\t\t".join('"%s"' % escape(p, True) for p in field)
-
-        self.include_paths = multiline(cpp_info.include_paths)
-        self.lib_paths = multiline(cpp_info.lib_paths)
-        self.res_paths = multiline(cpp_info.res_paths)
-        self.bin_paths = multiline(cpp_info.bin_paths)
-        self.build_paths = multiline(cpp_info.build_paths)
-
-        self.libs = escape(" ".join(cpp_info.libs))
-        self.defines = "\n\t\t\t".join("-D%s" % d for d in cpp_info.defines)
-        self.compile_definitions = "\n\t\t\t".join(cpp_info.defines)
-
-        self.cppflags = escape(" ".join(cpp_info.cppflags))
-        self.cflags = escape(" ".join(cpp_info.cflags))
-        self.sharedlinkflags = escape(" ".join(cpp_info.sharedlinkflags))
-        self.exelinkflags = escape(" ".join(cpp_info.exelinkflags))
+        def join_paths(paths):
+            # Paths are doubled quoted, and escaped (but spaces)
+            return "\n\t\t\t".join('"%s"' % p.replace('\\', '/').replace('$', '\\$').replace('"', '\\"')
+                                   for p in paths)
+
+        def join_flags(separator, values):
+            # Flags have to be escaped
+            return separator.join(v.replace('\\', '\\\\').replace('$', '\\$').replace('"', '\\"')
+                                  for v in values)
+
+        def join_defines(values, prefix=""):
+            # Defines have to be escaped, included spaces
+            return "\n\t\t\t".join('"%s%s"' % (prefix, v.replace('\\', '\\\\').replace('$', '\\$').
+                                   replace('"', '\\"'))
+                                   for v in values)
+
+        self.include_paths = join_paths(cpp_info.include_paths)
+        self.lib_paths = join_paths(cpp_info.lib_paths)
+        self.res_paths = join_paths(cpp_info.res_paths)
+        self.bin_paths = join_paths(cpp_info.bin_paths)
+        self.build_paths = join_paths(cpp_info.build_paths)
+
+        self.libs = join_flags(" ", cpp_info.libs)
+        self.defines = join_defines(cpp_info.defines, "-D")
+        self.compile_definitions = join_defines(cpp_info.defines)
+
+        self.cppflags = join_flags(" ", cpp_info.cppflags)
+        self.cflags = join_flags(" ", cpp_info.cflags)
+        self.sharedlinkflags = join_flags(" ", cpp_info.sharedlinkflags)
+        self.exelinkflags = join_flags(" ", cpp_info.exelinkflags)
 
         # For modern CMake targets we need to prepare a list to not
         # loose the elements in the list by replacing " " with ";". Example "-framework Foundation"
         # Issue: #1251
-        self.cppflags_list = escape(";".join(cpp_info.cppflags))
-        self.cflags_list = escape(";".join(cpp_info.cflags))
-        self.sharedlinkflags_list = escape(";".join(cpp_info.sharedlinkflags))
-        self.exelinkflags_list = escape(";".join(cpp_info.exelinkflags))
+        self.cppflags_list = join_flags(";", cpp_info.cppflags)
+        self.cflags_list = join_flags(";", cpp_info.cflags)
+        self.sharedlinkflags_list = join_flags(";", cpp_info.sharedlinkflags)
+        self.exelinkflags_list = join_flags(";", cpp_info.exelinkflags)
 
-        self.rootpath = '"%s"' % escape(cpp_info.rootpath, True)
+        self.rootpath = join_paths([cpp_info.rootpath])
 
 
 class CMakeGenerator(Generator):


Test patch:
diff --git a/conans/test/generators/cmake_test.py b/conans/test/generators/cmake_test.py
--- a/conans/test/generators/cmake_test.py
+++ b/conans/test/generators/cmake_test.py
@@ -33,10 +33,10 @@ def variables_setup_test(self):
         generator = CMakeGenerator(conanfile)
         content = generator.content
         cmake_lines = content.splitlines()
-        self.assertIn("set(CONAN_DEFINES_MYPKG -DMYDEFINE1)", cmake_lines)
-        self.assertIn("set(CONAN_DEFINES_MYPKG2 -DMYDEFINE2)", cmake_lines)
-        self.assertIn("set(CONAN_COMPILE_DEFINITIONS_MYPKG MYDEFINE1)", cmake_lines)
-        self.assertIn("set(CONAN_COMPILE_DEFINITIONS_MYPKG2 MYDEFINE2)", cmake_lines)
+        self.assertIn('set(CONAN_DEFINES_MYPKG "-DMYDEFINE1")', cmake_lines)
+        self.assertIn('set(CONAN_DEFINES_MYPKG2 "-DMYDEFINE2")', cmake_lines)
+        self.assertIn('set(CONAN_COMPILE_DEFINITIONS_MYPKG "MYDEFINE1")', cmake_lines)
+        self.assertIn('set(CONAN_COMPILE_DEFINITIONS_MYPKG2 "MYDEFINE2")', cmake_lines)
 
         self.assertIn('set(CONAN_USER_LIB1_myvar "myvalue")', cmake_lines)
         self.assertIn('set(CONAN_USER_LIB1_myvar2 "myvalue2")', cmake_lines)
@@ -96,12 +96,15 @@ def escaped_flags_test(self):
         cpp_info.includedirs.append("other_include_dir")
         cpp_info.cppflags = ["-load", r"C:\foo\bar.dll"]
         cpp_info.cflags = ["-load", r"C:\foo\bar2.dll"]
+        cpp_info.defines = ['MY_DEF=My string', 'MY_DEF2=My other string']
         conanfile.deps_cpp_info.update(cpp_info, ref.name)
         generator = CMakeGenerator(conanfile)
         content = generator.content
         cmake_lines = content.splitlines()
         self.assertIn(r'set(CONAN_C_FLAGS_MYPKG "-load C:\\foo\\bar2.dll")', cmake_lines)
         self.assertIn(r'set(CONAN_CXX_FLAGS_MYPKG "-load C:\\foo\\bar.dll")', cmake_lines)
+        self.assertIn(r'set(CONAN_DEFINES_MYPKG "-DMY_DEF=My string"', cmake_lines)
+        self.assertIn('\t\t\t"-DMY_DEF2=My other string")', cmake_lines)
 
     def aux_cmake_test_setup_test(self):
         conanfile = ConanFile(None, None, Settings({}), None)
diff --git a/conans/test/integration/cmake_flags_test.py b/conans/test/integration/cmake_flags_test.py
--- a/conans/test/integration/cmake_flags_test.py
+++ b/conans/test/integration/cmake_flags_test.py
@@ -4,6 +4,7 @@
 
 from conans.test.utils.tools import TestClient
 from nose.plugins.attrib import attr
+from parameterized.parameterized import parameterized
 
 
 conanfile_py = """
@@ -16,6 +17,7 @@ class HelloConan(ConanFile):
     def package_info(self):
         self.cpp_info.cppflags = ["MyFlag1", "MyFlag2"]
         self.cpp_info.cflags = ["-load", "C:\some\path"]
+        self.cpp_info.defines = ['MY_DEF=My" \string', 'MY_DEF2=My${} other \string']
 """
 
 chatconanfile_py = """
@@ -48,6 +50,8 @@ def package_info(self):
 message(STATUS "CONAN_C_FLAGS=${CONAN_C_FLAGS}")
 message(STATUS "HELLO_CXX_FLAGS=${HELLO_FLAGS}")
 message(STATUS "CHAT_CXX_FLAGS=${CHAT_FLAGS}")
+message(STATUS "CONAN_DEFINES_HELLO=${CONAN_DEFINES_HELLO}")
+message(STATUS "HELLO_DEFINES=${HELLO_DEFINES}")
 """
 
 
@@ -63,6 +67,59 @@ def _get_line(self, text, begin):
         self.assertNotIn('"', flags)
         return flags
 
+    @parameterized.expand([(True, ), (False, )])
+    def build_app_test(self, targets):
+        client = TestClient()
+        conanfile_py = """
+from conans import ConanFile
+
+class HelloConan(ConanFile):
+    name = "Hello"
+    version = "0.1"
+    def package_info(self):
+        self.cpp_info.defines = [r'MY_DEF=My${} $string', r'MY_DEF2=My$ other string']
+"""
+        client.save({"conanfile.py": conanfile_py})
+        client.run("create . lasote/testing")
+        consumer = """from conans import ConanFile, CMake
+import os
+class App(ConanFile):
+    settings = "os", "compiler", "arch", "build_type"
+    requires = "Hello/0.1@lasote/testing"
+    generators = "cmake"
+    def build(self):
+        cmake = CMake(self)
+        cmake.configure()
+        cmake.build()
+        self.run(os.sep.join([".", "bin", "myapp"]))
+"""
+        cmake_app = """set(CMAKE_CXX_COMPILER_WORKS 1)
+set(CMAKE_CXX_ABI_COMPILED 1)
+project(MyHello CXX)
+cmake_minimum_required(VERSION 2.8.12)
+
+include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
+conan_basic_setup(%s)
+add_executable(myapp myapp.cpp)
+conan_target_link_libraries(myapp)
+""" % ("TARGETS" if targets else "")
+
+        myapp = r"""#include <iostream>
+#define STRINGIFY(x) #x
+#define STRINGIFYMACRO(y) STRINGIFY(y)
+int main(){
+    std::cout << "Msg1: " << STRINGIFYMACRO(MY_DEF) << "\n";
+    std::cout << "Msg2: " << STRINGIFYMACRO(MY_DEF2) << "\n";
+}"""
+        client.save({"conanfile.py": consumer,
+                     "CMakeLists.txt": cmake_app,
+                     "myapp.cpp": myapp
+                     }, clean_first=True)
+        client.run("install .")
+        client.run("build .")
+        self.assertIn("Msg1: My${} $string", client.out)
+        self.assertIn("Msg2: My$ other string", client.out)
+
     def flags_test(self):
         client = TestClient()
         client.save({"conanfile.py": conanfile_py})
@@ -77,6 +134,8 @@ def flags_test(self):
         self.assertIn("CONAN_CXX_FLAGS=MyFlag1 MyFlag2", client.out)
         self.assertIn("CMAKE_C_FLAGS= -load C:\some\path", client.out)
         self.assertIn("CONAN_C_FLAGS=-load C:\some\path ", client.out)
+        self.assertIn('CONAN_DEFINES_HELLO=-DMY_DEF=My" \string;-DMY_DEF2=My${} other \string',
+                      client.out)
 
     def transitive_flags_test(self):
         client = TestClient()
@@ -101,19 +160,22 @@ def targets_flags_test(self):
         cmake_targets = cmake.replace("conan_basic_setup()",
                                       "conan_basic_setup(TARGETS)\n"
                                       "get_target_property(HELLO_FLAGS CONAN_PKG::Hello"
-                                      " INTERFACE_COMPILE_OPTIONS)")
+                                      " INTERFACE_COMPILE_OPTIONS)\n"
+                                      "get_target_property(HELLO_DEFINES CONAN_PKG::Hello"
+                                      " INTERFACE_COMPILE_DEFINITIONS)")
         client.save({"conanfile.txt": conanfile,
                      "CMakeLists.txt": cmake_targets},
                     clean_first=True)
 
         client.run('install . -g cmake')
         client.runner("cmake .", cwd=client.current_folder)
-        cmake_cxx_flags = self._get_line(client.user_io.out, "CMAKE_CXX_FLAGS")
+        cmake_cxx_flags = self._get_line(client.out, "CMAKE_CXX_FLAGS")
         self.assertNotIn("My", cmake_cxx_flags)
-        self.assertIn("CONAN_CXX_FLAGS=MyFlag1 MyFlag2", client.user_io.out)
+        self.assertIn("CONAN_CXX_FLAGS=MyFlag1 MyFlag2", client.out)
         self.assertIn("HELLO_CXX_FLAGS=-load;C:\some\path;MyFlag1;MyFlag2;"
                       "$<$<CONFIG:Release>:;>;$<$<CONFIG:RelWithDebInfo>:;>;"
                       "$<$<CONFIG:MinSizeRel>:;>;$<$<CONFIG:Debug>:;>", client.out)
+        self.assertIn('HELLO_DEFINES=MY_DEF=My" \string;MY_DEF2=My${} other \string;', client.out)
 
     def targets_own_flags_test(self):
         client = TestClient()
@@ -124,7 +186,9 @@ def targets_own_flags_test(self):
         cmake_targets = cmake.replace("conan_basic_setup()",
                                       "conan_basic_setup(TARGETS)\n"
                                       "get_target_property(HELLO_FLAGS CONAN_PKG::Hello"
-                                      " INTERFACE_COMPILE_OPTIONS)")
+                                      " INTERFACE_COMPILE_OPTIONS)\n"
+                                      "get_target_property(HELLO_DEFINES CONAN_PKG::Hello"
+                                      " INTERFACE_COMPILE_DEFINITIONS)")
         client.save({"conanfile.txt": conanfile,
                      "CMakeLists.txt": cmake_targets},
                     clean_first=True)
@@ -138,6 +202,7 @@ def targets_own_flags_test(self):
         self.assertIn("HELLO_CXX_FLAGS=-load;C:\some\path;MyFlag1;MyFlag2;"
                       "$<$<CONFIG:Release>:;>;$<$<CONFIG:RelWithDebInfo>:;>;"
                       "$<$<CONFIG:MinSizeRel>:;>;$<$<CONFIG:Debug>:;>", client.out)
+        self.assertIn('HELLO_DEFINES=MY_DEF=My" \string;MY_DEF2=My${} other \string;', client.out)
 
     def transitive_targets_flags_test(self):
         client = TestClient()
@@ -150,7 +215,9 @@ def transitive_targets_flags_test(self):
                                       "get_target_property(HELLO_FLAGS CONAN_PKG::Hello"
                                       " INTERFACE_COMPILE_OPTIONS)\n"
                                       "get_target_property(CHAT_FLAGS CONAN_PKG::Chat"
-                                      " INTERFACE_COMPILE_OPTIONS)\n")
+                                      " INTERFACE_COMPILE_OPTIONS)\n"
+                                      "get_target_property(HELLO_DEFINES CONAN_PKG::Hello"
+                                      " INTERFACE_COMPILE_DEFINITIONS)")
         client.save({"conanfile.txt": conanfile.replace("Hello", "Chat"),
                      "CMakeLists.txt": cmake_targets},
                     clean_first=True)
@@ -168,6 +235,7 @@ def transitive_targets_flags_test(self):
         self.assertIn("CHAT_CXX_FLAGS=MyChatFlag1;MyChatFlag2;"
                       "$<$<CONFIG:Release>:;>;$<$<CONFIG:RelWithDebInfo>:;>;"
                       "$<$<CONFIG:MinSizeRel>:;>;$<$<CONFIG:Debug>:;>", client.out)
+        self.assertIn('HELLO_DEFINES=MY_DEF=My" \string;MY_DEF2=My${} other \string;', client.out)
 
     def cmake_test_needed_settings(self):
         conanfile = """
diff --git a/conans/test/integration/cmake_targets_test.py b/conans/test/integration/cmake_targets_test.py
--- a/conans/test/integration/cmake_targets_test.py
+++ b/conans/test/integration/cmake_targets_test.py
@@ -89,7 +89,6 @@ def apple_framework_test(self):
         if platform.system() != "Darwin":
             return
 
-
         client = TestClient()
         conanfile_fr = conanfile_py + '''
     def package_info(self):