Repository: mesonbuild/meson

Issue:
Incorrect cmakedefine implementation
**Describe the bug**
the current cmakedefine implementation assumes that the definition name and value have to be the same
e.g.
`#cmakedefine VAR @VAR@`

**To Reproduce**
simply have any cmakedefine usage where the name and value mismatch
e.g.
`#cmakedefine const @HAVE_CONST@` (taken from SDL2 https://github.com/libsdl-org/SDL/blob/9d1dbd2ad86805c316cf4f2ee8ab2b361be18f4d/include/SDL_config.h.cmake#L34-L37)

**Expected behavior**
the value should be any definition value accepted by C 

This issue also affects muon, which is stricter about this and errors out.
Muon issue report: https://todo.sr.ht/~lattis/muon/77


Discussion:
This is a known issue, which mainly boils down to "cmakedefine is incredibly complicated in the confusing way".

It works for a subset of well-designed cmakedefine templates. I talked about this a bit at https://github.com/mesonbuild/wrapdb/issues/708#issuecomment-1286498712

You can see my personal policy on fixing this, there. :D
(My personal feeling on the matter is to use handwritten mesondefine templates.)
I've gone ahead and made a small test repo for this to test edge cases against cmake and meson
https://github.com/Jan200101/cmakedefine-test

The issues it catches thus far are:
- unimplemented cmakedefine01 (cmakedefine01 treated as cmakedefine)
- incorrect handling of the definition body
- incorrect whitespace handling (like with `#define`, the hash (#) and `cmakedefine` keyword can be seperated by whitespace)

I'll find some more edge cases to fix and later implement tests for them in the unittests


Resolving patch:
diff --git a/mesonbuild/utils/universal.py b/mesonbuild/utils/universal.py
--- a/mesonbuild/utils/universal.py
+++ b/mesonbuild/utils/universal.py
@@ -1207,6 +1207,8 @@ def variable_replace(match: T.Match[str]) -> str:
                 var, _ = confdata.get(varname)
                 if isinstance(var, str):
                     var_str = var
+                elif isinstance(var, bool):
+                    var_str = str(int(var))
                 elif isinstance(var, int):
                     var_str = str(var)
                 else:
@@ -1220,8 +1222,17 @@ def variable_replace(match: T.Match[str]) -> str:
 
 def do_define(regex: T.Pattern[str], line: str, confdata: 'ConfigurationData',
               variable_format: Literal['meson', 'cmake', 'cmake@'], subproject: T.Optional[SubProject] = None) -> str:
+    cmake_bool_define = False
+    if variable_format != "meson":
+        cmake_bool_define = "cmakedefine01" in line
+
     def get_cmake_define(line: str, confdata: 'ConfigurationData') -> str:
         arr = line.split()
+
+        if cmake_bool_define:
+            (v, desc) = confdata.get(arr[1])
+            return str(int(bool(v)))
+
         define_value: T.List[str] = []
         for token in arr[2:]:
             try:
@@ -1243,22 +1254,29 @@ def get_cmake_define(line: str, confdata: 'ConfigurationData') -> str:
     try:
         v, _ = confdata.get(varname)
     except KeyError:
-        return '/* #undef %s */\n' % varname
-    if isinstance(v, bool):
-        if v:
-            return '#define %s\n' % varname
+        if cmake_bool_define:
+            return '#define %s 0\n' % varname
         else:
-            return '#undef %s\n' % varname
-    elif isinstance(v, int):
-        return '#define %s %d\n' % (varname, v)
-    elif isinstance(v, str):
+            return '/* #undef %s */\n' % varname
+
+    if isinstance(v, str) or variable_format != "meson":
         if variable_format == 'meson':
             result = v
         else:
+            if not cmake_bool_define and not v:
+                return '/* #undef %s */\n' % varname
+
             result = get_cmake_define(line, confdata)
-        result = f'#define {varname} {result}\n'
+        result = f'#define {varname} {result}'.strip() + '\n'
         result, _ = do_replacement(regex, result, variable_format, confdata)
         return result
+    elif isinstance(v, bool):
+        if v:
+            return '#define %s\n' % varname
+        else:
+            return '#undef %s\n' % varname
+    elif isinstance(v, int):
+        return '#define %s %d\n' % (varname, v)
     else:
         raise MesonException('#mesondefine argument "%s" is of unknown type.' % varname)
 
@@ -1295,7 +1313,7 @@ def line_is_valid(line: str, variable_format: str) -> bool:
     # during substitution so we can warn the user to use the `copy:` kwarg.
     confdata_useless = not confdata.keys()
     for line in data:
-        if line.startswith(search_token):
+        if line.lstrip().startswith(search_token):
             confdata_useless = False
             line = do_define(regex, line, confdata, variable_format, subproject)
         else:
diff --git a/unittests/allplatformstests.py b/unittests/allplatformstests.py
--- a/unittests/allplatformstests.py
+++ b/unittests/allplatformstests.py
@@ -153,20 +153,33 @@ def conf_str(in_data, confdata, vformat):
             (result, missing_variables, confdata_useless) = do_conf_str('configuration_file', in_data, confdata, variable_format = vformat)
             return '\n'.join(result)
 
-        def check_formats(confdata, result):
+        def check_meson_format(confdata, result):
             self.assertEqual(conf_str(['#mesondefine VAR'], confdata, 'meson'), result)
+
+        def check_cmake_format_simple(confdata, result):
+            self.assertEqual(conf_str(['#cmakedefine VAR'], confdata, 'cmake'), result)
+
+        def check_cmake_formats_full(confdata, result):
             self.assertEqual(conf_str(['#cmakedefine VAR ${VAR}'], confdata, 'cmake'), result)
             self.assertEqual(conf_str(['#cmakedefine VAR @VAR@'], confdata, 'cmake@'), result)
 
+        def check_formats(confdata, result):
+            check_meson_format(confdata, result)
+            check_cmake_formats_full(confdata, result)
+
         confdata = ConfigurationData()
         # Key error as they do not exists
         check_formats(confdata, '/* #undef VAR */\n')
 
         # Check boolean
         confdata.values = {'VAR': (False, 'description')}
-        check_formats(confdata, '#undef VAR\n')
+        check_meson_format(confdata, '#undef VAR\n')
+        check_cmake_formats_full(confdata, '/* #undef VAR */\n')
+
         confdata.values = {'VAR': (True, 'description')}
-        check_formats(confdata, '#define VAR\n')
+        check_meson_format(confdata, '#define VAR\n')
+        check_cmake_format_simple(confdata, '#define VAR\n')
+        check_cmake_formats_full(confdata, '#define VAR 1\n')
 
         # Check string
         confdata.values = {'VAR': ('value', 'description')}
@@ -176,6 +189,25 @@ def check_formats(confdata, result):
         confdata.values = {'VAR': (10, 'description')}
         check_formats(confdata, '#define VAR 10\n')
 
+        # Checking if cmakedefine behaves as it does with cmake
+        confdata.values = {'VAR': ("var", 'description')}
+        self.assertEqual(conf_str(['#cmakedefine VAR @VAR@'], confdata, 'cmake@'), '#define VAR var\n')
+
+        confdata.values = {'VAR': (True, 'description')}
+        self.assertEqual(conf_str(['#cmakedefine01 VAR'], confdata, 'cmake'), '#define VAR 1\n')
+
+        confdata.values = {'VAR': (0, 'description')}
+        self.assertEqual(conf_str(['#cmakedefine01 VAR'], confdata, 'cmake'), '#define VAR 0\n')
+        confdata.values = {'VAR': (False, 'description')}
+        self.assertEqual(conf_str(['#cmakedefine01 VAR'], confdata, 'cmake'), '#define VAR 0\n')
+
+        confdata.values = {}
+        self.assertEqual(conf_str(['#cmakedefine01 VAR'], confdata, 'cmake'), '#define VAR 0\n')
+        self.assertEqual(conf_str(['#cmakedefine VAR @VAR@'], confdata, 'cmake@'), '/* #undef VAR */\n')
+
+        confdata.values = {'VAR': (5, 'description')}
+        self.assertEqual(conf_str(['#cmakedefine VAR'], confdata, 'cmake'), '#define VAR\n')
+
         # Check multiple string with cmake formats
         confdata.values = {'VAR': ('value', 'description')}
         self.assertEqual(conf_str(['#cmakedefine VAR xxx @VAR@ yyy @VAR@'], confdata, 'cmake@'), '#define VAR xxx value yyy value\n')


Test patch: