Repository: mesonbuild/meson

Issue:
Allow `meson configure` in an uninitialised build dir
Currently, running `meson configure` in an uninitialised builddir (such as the srcdir) results in
> Directory /path/to/project/ does not seem to be a Meson build directory.

This is an issue when a user clones a project and tries to configure it for the first time, but is missing dependencies for components that can be turned off. They have no way of listing the options to know how to turn one off and make the initial `meson builddir/` command pass, as `meson configure` refuses to list them.

IMO the condition should be relaxed, disallowing *setting an option* from the srcdir, but letting users *list options*.

I'm happy to give this a stab, I just want a green light first :)

(Cc: @imirkin, who raised the issue in the Mesa project)


Discussion:
run `meson configure` in srcdir should list configure options of current project, just like ` ./configure -h` 
@liugang I'm not sure if you're just reiterating my request or saying this is already supported, but it's not, you can test this yourself :)
I mean not supported
The first step is to add parsing of `meson_options.txt` and printing it in `--help`, see https://github.com/mesonbuild/meson/issues/2402.
Meson currently (as in it is in master) supports extracting all options via the introspection API. However, the output is in JSON. It should be possible to make this usable with `meson configure`.

Resolving patch:
diff --git a/mesonbuild/mconf.py b/mesonbuild/mconf.py
--- a/mesonbuild/mconf.py
+++ b/mesonbuild/mconf.py
@@ -13,8 +13,7 @@
 # limitations under the License.
 
 import os
-from . import (coredata, mesonlib, build)
-from . import mintro
+from . import coredata, environment, mesonlib, build, mintro, mlog
 
 def add_arguments(parser):
     coredata.register_builtin_arguments(parser)
@@ -38,11 +37,28 @@ class ConfException(mesonlib.MesonException):
 
 class Conf:
     def __init__(self, build_dir):
-        self.build_dir = build_dir
-        if not os.path.isdir(os.path.join(build_dir, 'meson-private')):
-            raise ConfException('Directory %s does not seem to be a Meson build directory.' % build_dir)
-        self.build = build.load(self.build_dir)
-        self.coredata = coredata.load(self.build_dir)
+        self.build_dir = os.path.abspath(os.path.realpath(build_dir))
+        if 'meson.build' in [os.path.basename(self.build_dir), self.build_dir]:
+            self.build_dir = os.path.dirname(self.build_dir)
+        self.build = None
+
+        if os.path.isdir(os.path.join(self.build_dir, 'meson-private')):
+            self.build = build.load(self.build_dir)
+            self.source_dir = self.build.environment.get_source_dir()
+            self.coredata = coredata.load(self.build_dir)
+            self.default_values_only = False
+        elif os.path.isfile(os.path.join(self.build_dir, environment.build_filename)):
+            # Make sure that log entries in other parts of meson don't interfere with the JSON output
+            mlog.disable()
+            self.source_dir = os.path.abspath(os.path.realpath(self.build_dir))
+            intr = mintro.IntrospectionInterpreter(self.source_dir, '', 'ninja')
+            intr.analyze()
+            # Reenable logging just in case
+            mlog.enable()
+            self.coredata = intr.coredata
+            self.default_values_only = True
+        else:
+            raise ConfException('Directory {} is neither a Meson build directory nor a project source directory.'.format(build_dir))
 
     def clear_cache(self):
         self.coredata.deps = {}
@@ -51,18 +67,22 @@ def set_options(self, options):
         self.coredata.set_options(options)
 
     def save(self):
+        # Do nothing when using introspection
+        if self.default_values_only:
+            return
         # Only called if something has changed so overwrite unconditionally.
         coredata.save(self.coredata, self.build_dir)
         # We don't write the build file because any changes to it
         # are erased when Meson is executed the next time, i.e. when
         # Ninja is run.
 
-    @staticmethod
-    def print_aligned(arr):
+    def print_aligned(self, arr):
         if not arr:
             return
 
         titles = {'name': 'Option', 'descr': 'Description', 'value': 'Current Value', 'choices': 'Possible Values'}
+        if self.default_values_only:
+            titles['value'] = 'Default Value'
 
         name_col = [titles['name'], '-' * len(titles['name'])]
         value_col = [titles['value'], '-' * len(titles['value'])]
@@ -111,9 +131,18 @@ def print_options(self, title, options):
         self.print_aligned(arr)
 
     def print_conf(self):
+        def print_default_values_warning():
+            mlog.warning('The source directory instead of the build directory was specified.')
+            mlog.warning('Only the default values for the project are printed, and all command line parameters are ignored.')
+
+        if self.default_values_only:
+            print_default_values_warning()
+            print('')
+
         print('Core properties:')
-        print('  Source dir', self.build.environment.source_dir)
-        print('  Build dir ', self.build.environment.build_dir)
+        print('  Source dir', self.source_dir)
+        if not self.default_values_only:
+            print('  Build dir ', self.build_dir)
 
         dir_option_names = ['bindir',
                             'datadir',
@@ -144,6 +173,10 @@ def print_conf(self):
         self.print_options('Project options', self.coredata.user_options)
         self.print_options('Testing options', test_options)
 
+        # Print the warning twice so that the user shouldn't be able to miss it
+        if self.default_values_only:
+            print('')
+            print_default_values_warning()
 
 def run(options):
     coredata.parse_cmd_line_options(options)
@@ -151,6 +184,10 @@ def run(options):
     c = None
     try:
         c = Conf(builddir)
+        if c.default_values_only:
+            c.print_conf()
+            return 0
+
         save = False
         if len(options.cmd_line_options) > 0:
             c.set_options(options.cmd_line_options)
diff --git a/mesonbuild/mintro.py b/mesonbuild/mintro.py
--- a/mesonbuild/mintro.py
+++ b/mesonbuild/mintro.py
@@ -441,7 +441,7 @@ def run(options):
     if options.builddir is not None:
         datadir = os.path.join(options.builddir, datadir)
         infodir = os.path.join(options.builddir, infodir)
-    if options.builddir.endswith('/meson.build') or options.builddir.endswith('\\meson.build') or options.builddir == 'meson.build':
+    if 'meson.build' in [os.path.basename(options.builddir), options.builddir]:
         sourcedir = '.' if options.builddir == 'meson.build' else options.builddir[:-11]
         if options.projectinfo:
             list_projinfo_from_source(sourcedir, indent)


Test patch: