Repository: mesonbuild/meson

Issue:
Allow configuration_file() without config.h.meson (or input file)
It would be great if we could make it so that we can write config.h files without manually maintaining a config.h.meson input file, which is error prone. Also, autoconf doesn't make us do that ;)

Case in point: defines that determined symbol visibility weren't written into config.h because the config.h.meson file wasn't fully up-to-date, and meson users then wondered what went wrong during linking of some tests that symbols didn't show up etc. But usually such things fail in much more subtle ways.



Discussion:
How would you define what to write in the file? Suppose you set `FOOBAR` to `"abc"`. What should go in the config file? `#define FOOBAR abc`? `#define FOOBAR "abc"`? `#define HAVE_FOOBAR 1`? If it is missing, how would you know to write `#undef FOOBAR` or `#define FOOBAR 0` or what have you? There seem to be a ton of different ways to do this which are all slightly incompatible with each other.

Also doesn't Autoconf determine a config.h.in by scanning all the sources as the first step? That is humongously slow, we won't be doing that (and parsing C/C++ reliably is a nightmare to boot).

I don't think we should ever scan sources to generate the `config.h`. That was a terrible idea in the first place. I think we can define the behaviour of `configure_file()` for all  ambiguities:

> Suppose you set FOOBAR to "abc".

If you do `conf.set('FOOBAR', '"abc"')`, then the config file should have `#define FOOBAR "abc"`. i.e., if it's a string, just literally put the string there. This is what we already do anyway. I think this is a full list:

`conf.set('FOOBAR', '"string"')` should become `#define FOOBAR "string"`

`conf.set('FOOBAR', 'string')` should become `#define FOOBAR string`

`conf.set('FOOBAR', true)` should become `#define FOOBAR`

`conf.set('FOOBAR', false)` should become `#undef FOOBAR`

`conf.set('FOOBAR', 1)` should become `#define FOOBAR 1`

`conf.set('FOOBAR', 0)` should become `#define FOOBAR 0`

`conf.set('FOOBAR', "1")` should become `#define FOOBAR "1"` (and so on)

I think that covers all cases?

Seems quite sensible. How should we expose this? Ideas that come to mind from most favorite to least:
- `configure_file` without an input file
- ´cdata.to_header('config.h')`
- `generate_header(cdata)`

I personally had imagined it being implemented as the first option: 

>  `configure_file` without an input file

It's quite elegant to do this.


Resolving patch:
diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py
--- a/mesonbuild/interpreter.py
+++ b/mesonbuild/interpreter.py
@@ -1887,13 +1887,11 @@ def func_install_subdir(self, node, args, kwargs):
     def func_configure_file(self, node, args, kwargs):
         if len(args) > 0:
             raise InterpreterException("configure_file takes only keyword arguments.")
-        if not 'input' in kwargs:
-            raise InterpreterException('Required keyword argument "input" not defined.')
         if not 'output' in kwargs:
             raise InterpreterException('Required keyword argument "output" not defined.')
-        inputfile = kwargs['input']
+        inputfile = kwargs.get('input', None)
         output = kwargs['output']
-        if not isinstance(inputfile, str):
+        if not isinstance(inputfile, (str, type(None))):
             raise InterpreterException('Input must be a string.')
         if not isinstance(output, str):
             raise InterpreterException('Output must be a string.')
@@ -1903,16 +1901,20 @@ def func_configure_file(self, node, args, kwargs):
             conf = kwargs['configuration']
             if not isinstance(conf, ConfigurationDataHolder):
                 raise InterpreterException('Argument "configuration" is not of type configuration_data')
-
-            conffile = os.path.join(self.subdir, inputfile)
-            if conffile not in self.build_def_files:
-                self.build_def_files.append(conffile)
-            os.makedirs(os.path.join(self.environment.build_dir, self.subdir), exist_ok=True)
-            ifile_abs = os.path.join(self.environment.source_dir, self.subdir, inputfile)
             ofile_abs = os.path.join(self.environment.build_dir, self.subdir, output)
-            mesonlib.do_conf_file(ifile_abs, ofile_abs, conf.held_object)
+            if inputfile is not None:
+                conffile = os.path.join(self.subdir, inputfile)
+                if conffile not in self.build_def_files:
+                    self.build_def_files.append(conffile)
+                os.makedirs(os.path.join(self.environment.build_dir, self.subdir), exist_ok=True)
+                ifile_abs = os.path.join(self.environment.source_dir, self.subdir, inputfile)
+                mesonlib.do_conf_file(ifile_abs, ofile_abs, conf.held_object)
+            else:
+                mesonlib.dump_conf_header(ofile_abs, conf.held_object)
             conf.mark_used()
         elif 'command' in kwargs:
+            if 'input' not in kwargs:
+                raise InterpreterException('Required keyword input missing.')
             res = self.func_run_command(node, kwargs['command'], {})
             if res.returncode != 0:
                 raise InterpreterException('Running configure command failed.\n%s\n%s' %
diff --git a/mesonbuild/mesonlib.py b/mesonbuild/mesonlib.py
--- a/mesonbuild/mesonlib.py
+++ b/mesonbuild/mesonlib.py
@@ -267,6 +267,27 @@ def do_conf_file(src, dst, confdata):
     shutil.copymode(src, dst_tmp)
     replace_if_different(dst, dst_tmp)
 
+def dump_conf_header(ofilename, cdata):
+    with open(ofilename, 'w') as ofile:
+        ofile.write('''/*
+ * Autogenerated by the Meson build system.
+ * Do not edit, your changes will be lost.
+ */
+
+#pragma once
+
+''')
+        for k in sorted(cdata.keys()):
+            v = cdata.get(k)
+            if isinstance(v, bool):
+                if v:
+                    ofile.write('#define %s\n\n' % k)
+                else:
+                    ofile.write('#undef %s\n\n' % k)
+            elif isinstance(v, (int, str)):
+                ofile.write('#define %s %s\n\n' % (k, v))
+            else:
+                raise MesonException('Unknown data type in configuration file entry: ' + k)
 
 def replace_if_different(dst, dst_tmp):
     # If contents are identical, don't touch the file to prevent


Test patch:
diff --git a/test cases/common/16 configure file/dumpprog.c b/test cases/common/16 configure file/dumpprog.c
new file mode 100644
--- /dev/null
+++ b/test cases/common/16 configure file/dumpprog.c	
@@ -0,0 +1,33 @@
+#define SHOULD_BE_UNDEFINED 1
+
+#include"config3.h"
+#include<string.h>
+#include<stdio.h>
+
+#ifdef SHOULD_BE_UNDEFINED
+#error Token did not get undefined.
+#endif
+
+#ifndef SHOULD_BE_DEFINED
+#error Token did not get defined
+#endif
+
+int main(int argc, char **argv) {
+    if(strcmp(SHOULD_BE_STRING, "string") != 0) {
+        printf("String token defined wrong.\n");
+        return 1;
+    }
+    if(SHOULD_BE_ONE != 1) {
+        printf("One defined incorrectly.\n");
+        return 1;
+    }
+    if(SHOULD_BE_ZERO != 0) {
+        printf("Zero defined incorrectly.\n");
+        return 1;
+    }
+    if(strcmp(SHOULD_BE_QUOTED_ONE, "1") != 0) {
+        printf("Quoted number defined incorrectly.\n");
+        return 1;
+    }
+    SHOULD_BE_RETURN 0;
+}
diff --git a/test cases/common/16 configure file/meson.build b/test cases/common/16 configure file/meson.build
--- a/test cases/common/16 configure file/meson.build	
+++ b/test cases/common/16 configure file/meson.build	
@@ -30,3 +30,18 @@ command : [genprog, scriptfile,  ifile, ofile],
 install_dir : 'share/appdir')
 
 test('inctest2', executable('prog2', 'prog2.c'))
+
+# Generate a conf file without an input file.
+
+dump = configuration_data()
+dump.set('SHOULD_BE_STRING', '"string"')
+dump.set('SHOULD_BE_RETURN', 'return')
+dump.set('SHOULD_BE_DEFINED', true)
+dump.set('SHOULD_BE_UNDEFINED', false)
+dump.set('SHOULD_BE_ONE', 1)
+dump.set('SHOULD_BE_ZERO', 0)
+dump.set('SHOULD_BE_QUOTED_ONE', '"1"')
+configure_file(output : 'config3.h',
+  configuration : dump)
+
+test('Configless.', executable('dumpprog', 'dumpprog.c'))