Repository: mesonbuild/meson

Issue:
Feature request: allow reading the build options line from inside meson.build
Hey!

I'm working on some enhancements to a wrap, and one of the missing features is storing the configuration line inside the config.h header. Meson already saves this information in the build log, so currently I'm using a hack to extract this information at setup time:

```python

import re
import os

if __name__ == '__main__':
     with open(f'{os.environ["MESON_BUILD_ROOT"]}/meson-logs/meson-log.txt', encoding="utf-8", mode='r') as f:
        lines = [line for line in f if 'Build Options' in line]

        options = re.match(r'Build Options:\s+(.+)$', lines[-1])

        if options is not None:
            print(options.group(1))
```

And then in meson.build:
```meson
extractor = find_program('capture_build_options.py', required: true)
configuration = run_command(extractor)
assert(configuration.returncode() == 0, 'Could not extract configuration string from meson!')

final_conf = configuration_data()
final_conf.set_quoted('CONFIGURATION', configuration.stdout().strip())
configure_file(
  output: 'config.h',
  configuration: final_conf,
)
```

Would it be possible to add a read-only variable, perhaps to the `meson` object, to access this information?


Discussion:
:+1: from me, I think it is fairly common for command-line tools to display the options they were built with when running, the context here is GStreamer's FFmpeg meson port for what it's worth.
We do this with a [post conf script](https://keep.imfreedom.org/pidgin/pidgin/file/tip/mkmesonconf.py) that calls `meson introspect` I'm not sure how robust that is, but it seems to work.
@QuLogic note that there is a difference between both solutions, the former only shows user defined otions, not default options. If we add an explicit function for this, I guess we would need an argument specifying if you want to include defaults or not.
> We do this with a [post conf script](https://keep.imfreedom.org/pidgin/pidgin/file/tip/mkmesonconf.py) that calls `meson introspect` I'm not sure how robust that is, but it seems to work.

This approach works well, if you can generate the header during the build process.

Apart from that restriction, its weakness that it doesn't scale with the number of options; in our case it'll generate over *2000* option entries, most of which depend on the value of`-Dauto_features=<default>` anyway.
So as I understand it, this is about documenting what options [a project like ffmpeg](https://gitlab.freedesktop.org/gstreamer/meson-ports/ffmpeg/-/merge_requests/71) was built with? Is the purpose of that purely to document the project-specific options that were used? Why does it go into the header?

Trying to understand the precise use-case here, so we can solve it better.
It's typically emitted as part of the --version text of some programs, so it has to be embedded in a generated header to end up in the binary.
That's not a purpose, that's just another chain in the implementation. What is the purpose of putting it in the version text? So that when users come to you for support you know how the app was built?

Do you want just the project-specific options there or also, for example, the optimization args? What about options from cross files and native files? What about env vars such as CFLAGS / LDFLAGS? Do you want to serialize the entire build state and put it in a header?

Resolving patch:
diff --git a/mesonbuild/interpreter/mesonmain.py b/mesonbuild/interpreter/mesonmain.py
--- a/mesonbuild/interpreter/mesonmain.py
+++ b/mesonbuild/interpreter/mesonmain.py
@@ -9,7 +9,7 @@
 from .. import mesonlib
 from .. import dependencies
 from .. import build
-from .. import mlog
+from .. import mlog, coredata
 
 from ..mesonlib import MachineChoice, OptionKey
 from ..programs import OverrideProgram, ExternalProgram
@@ -84,6 +84,7 @@ def __init__(self, build: 'build.Build', interpreter: 'Interpreter'):
                              'has_external_property': self.has_external_property_method,
                              'backend': self.backend_method,
                              'add_devenv': self.add_devenv_method,
+                             'build_options': self.build_options_method,
                              })
 
     def _find_source_script(
@@ -465,3 +466,12 @@ def add_devenv_method(self, args: T.Tuple[T.Union[str, list, dict, build.Environ
         converted = env_convertor_with_method(env, kwargs['method'], kwargs['separator'])
         assert isinstance(converted, build.EnvironmentVariables)
         self.build.devenv.append(converted)
+
+    @noPosargs
+    @noKwargs
+    @FeatureNew('meson.build_options', '1.1.0')
+    def build_options_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
+        options = self.interpreter.user_defined_options
+        if options is None:
+            return ''
+        return coredata.format_cmd_line_options(options)
diff --git a/unittests/allplatformstests.py b/unittests/allplatformstests.py
--- a/unittests/allplatformstests.py
+++ b/unittests/allplatformstests.py
@@ -1948,6 +1948,33 @@ def test_options_with_choices_changing(self) -> None:
                 self.assertEqual(item['value'], ['b', 'c'])
                 self.assertEqual(item['choices'], ['b', 'c', 'd'])
 
+    def test_options_listed_in_build_options(self) -> None:
+        """Detect when changed options become listed in build options."""
+        testdir = os.path.join(self.unit_test_dir, '110 list build options')
+
+        out = self.init(testdir)
+        for line in out.splitlines():
+            if line.startswith('Message: Build options:'):
+                self.assertNotIn('-Dauto_features=auto', line)
+                self.assertNotIn('-Doptional=auto', line)
+
+        self.wipe()
+        self.mac_ci_delay()
+
+        out = self.init(testdir, extra_args=['-Dauto_features=disabled', '-Doptional=enabled'])
+        for line in out.splitlines():
+            if line.startswith('Message: Build options:'):
+                self.assertIn('-Dauto_features=disabled', line)
+                self.assertIn('-Doptional=enabled', line)
+
+        self.setconf('-Doptional=disabled')
+        out = self.build()
+        for line in out.splitlines():
+            if line.startswith('Message: Build options:'):
+                self.assertIn('-Dauto_features=disabled', line)
+                self.assertNotIn('-Doptional=enabled', line)
+                self.assertIn('-Doptional=disabled', line)
+
     def test_subproject_promotion(self):
         testdir = os.path.join(self.unit_test_dir, '12 promote')
         workdir = os.path.join(self.builddir, 'work')


Test patch:
diff --git a/test cases/unit/110 list build options/meson.build b/test cases/unit/110 list build options/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/unit/110 list build options/meson.build	
@@ -0,0 +1,6 @@
+project('feature user option', 'c')
+
+feature_opts = get_option('auto_features')
+optional_opt = get_option('optional')
+
+message('Build options:', meson.build_options())
diff --git a/test cases/unit/110 list build options/meson_options.txt b/test cases/unit/110 list build options/meson_options.txt
new file mode 100644
--- /dev/null
+++ b/test cases/unit/110 list build options/meson_options.txt	
@@ -0,0 +1 @@
+option('optional', type : 'feature', value : 'auto', description : 'An optional feature')