[gnome-builder/wip/tingping/meson] meson: Initial plugin



commit 2e7a42949b070c0e39fae5f99a356f7c21348c2c
Author: Patrick Griffis <tingping tingping se>
Date:   Wed Aug 17 16:42:48 2016 -0400

    meson: Initial plugin
    
    This should support everything:
    
    - Building
    - Installing
    - Targets (Running)
    - Getting compile flags (Clang)
    
    https://bugzilla.gnome.org/show_bug.cgi?id=743280

 configure.ac                           |    2 +
 plugins/meson/Makefile.am              |   14 ++
 plugins/meson/configure.ac             |   12 +
 plugins/meson/meson.plugin             |   11 +
 plugins/meson/meson_plugin/__init__.py |  370 ++++++++++++++++++++++++++++++++
 5 files changed, 409 insertions(+), 0 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 61b6bbf..e0bd388 100644
--- a/configure.ac
+++ b/configure.ac
@@ -304,6 +304,7 @@ m4_include([plugins/html-completion/configure.ac])
 m4_include([plugins/html-preview/configure.ac])
 m4_include([plugins/jedi/configure.ac])
 m4_include([plugins/jhbuild/configure.ac])
+m4_include([plugins/meson/configure.ac])
 m4_include([plugins/mingw/configure.ac])
 m4_include([plugins/project-tree/configure.ac])
 m4_include([plugins/python-gi-imports-completion/configure.ac])
@@ -612,6 +613,7 @@ echo "  GNOME Code Assistance ................ : ${enable_gnome_code_assistance_
 echo "  HTML Autocompletion .................. : ${enable_html_completion_plugin}"
 echo "  HTML and Markdown Preview ............ : ${enable_html_preview_plugin}"
 echo "  JHBuild .............................. : ${enable_jhbuild_plugin}"
+echo "  Meson ................................ : ${enable_meson_plugin}"
 echo "  MinGW ................................ : ${enable_mingw_plugin}"
 echo "  Project Creation ..................... : ${enable_create_project_plugin}"
 echo "  Project Tree ......................... : ${enable_project_tree_plugin}"
diff --git a/plugins/meson/Makefile.am b/plugins/meson/Makefile.am
new file mode 100644
index 0000000..a38b3ac
--- /dev/null
+++ b/plugins/meson/Makefile.am
@@ -0,0 +1,14 @@
+if ENABLE_MESON_PLUGIN
+
+EXTRA_DIST = $(plugin_DATA)
+
+plugindir = $(libdir)/gnome-builder/plugins
+dist_plugin_DATA = meson.plugin
+
+moduledir = $(libdir)/gnome-builder/plugins/meson_plugin
+dist_module_DATA = meson_plugin/__init__.py
+
+endif
+
+-include $(top_srcdir)/git.mk
+
diff --git a/plugins/meson/configure.ac b/plugins/meson/configure.ac
new file mode 100644
index 0000000..6950ae2
--- /dev/null
+++ b/plugins/meson/configure.ac
@@ -0,0 +1,12 @@
+# --enable-meson-plugin=yes/no
+AC_ARG_ENABLE([meson-plugin],
+              [AS_HELP_STRING([--enable-meson-plugin=@<:@yes/no@:>@],
+                              [Build with support for the Meson build system])],
+              [enable_meson_plugin=$enableval],
+              [enable_meson_plugin=yes])
+
+# for if ENABLE_MESON_PLUGIN in Makefile.am
+AM_CONDITIONAL(ENABLE_MESON_PLUGIN, test x$enable_python_scripting = xyes && test x$enable_meson_plugin = 
xyes)
+
+# Ensure our makefile is generated by autoconf
+AC_CONFIG_FILES([plugins/meson/Makefile])
diff --git a/plugins/meson/meson.plugin b/plugins/meson/meson.plugin
new file mode 100644
index 0000000..081a4b9
--- /dev/null
+++ b/plugins/meson/meson.plugin
@@ -0,0 +1,11 @@
+[Plugin]
+Module=meson_plugin
+Loader=python3
+Name=Meson
+Description=Provides integration with the Meson build system
+Authors=Patrick Griffis <tingping tingping se>
+Copyright=Copyright © 2016 Patrick Griffis
+Builtin=true
+Hidden=true
+X-Project-File-Filter-Pattern=meson.build
+X-Project-File-Filter-Name=Meson Project (meson.build)
diff --git a/plugins/meson/meson_plugin/__init__.py b/plugins/meson/meson_plugin/__init__.py
new file mode 100644
index 0000000..de9d0d6
--- /dev/null
+++ b/plugins/meson/meson_plugin/__init__.py
@@ -0,0 +1,370 @@
+# __init__.py
+#
+# Copyright (C) 2016 Patrick Griffis <tingping tingping se>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from os import path
+import subprocess
+import threading
+import shutil
+import json
+import gi
+
+gi.require_version('Ide', '1.0')
+
+from gi.repository import (
+    GLib,
+    GObject,
+    Gio,
+    Ide
+)
+
+_ = Ide.gettext
+
+ninja = None
+
+
+class MesonBuildSystem(Ide.Object, Ide.BuildSystem, Gio.AsyncInitable):
+    project_file = GObject.Property(type=Gio.File)
+
+    def do_init_async(self, priority, cancel, callback, data=None):
+        task = Gio.Task.new(self, cancel, callback)
+        task.set_priority(priority)
+
+        self._cached_config = None
+        self._cached_builder = None
+
+        # TODO: Be async here also
+        project_file = self.get_context().get_project_file()
+        if project_file.get_basename() == 'meson.build':
+            task.return_boolean(True)
+        else:
+            child = project_file.get_child('meson.build')
+            exists = child.query_exists(cancel)
+            if exists:
+                self.props.project_file = child
+            task.return_boolean(exists)
+
+    def do_init_finish(self, result):
+        return result.propagate_boolean()
+
+    def do_get_priority(self):
+        return -200 # Lower priority than Autotools for now
+
+    def do_get_builder(self, config):
+        if config == self._cached_config:
+            return self._cached_builder
+        else:
+            self._cached_config = config
+            self._cached_builder = MesonBuilder(context=self.get_context(), configuration=config)
+            return self._cached_builder
+
+    def do_get_build_flags_async(self, ifile, cancellable, callback, data=None):
+        task = Gio.Task.new(self, cancellable, callback)
+        task.build_flags = []
+
+        config = self._cached_config
+        builder = self._cached_builder
+
+        # FIXME: task.return_boolean(False) segfaults?
+        if not config:
+            task.return_error(GLib.Error('Project must be built before we can get flags'))
+            return
+
+        def extract_flags(command: str):
+            flags = GLib.shell_parse_argv(command)[1] # Raises on failure
+            return [flag for flag in flags if flag.startswith(('-I', '-isystem', '-W', '-D'))]
+
+        def build_flags_thread():
+            commands_file = path.join(builder._get_build_dir().get_path(), 'compile_commands.json')
+            try:
+                with open(commands_file) as f:
+                    commands = json.loads(f.read(), encoding='utf-8')
+            except (json.JSONDecodeError, FileNotFoundError):
+                task.return_error(GLib.Error('Failed to decode meson json'))
+                return
+
+            infile = ifile.get_path()
+            for c in commands:
+                filepath = path.normpath(path.join(c['directory'], c['file']))
+                if filepath == infile:
+                    try:
+                        task.build_flags = extract_flags(c['command'])
+                    except GLib.Error as e:
+                        task.return_error(e)
+                        return
+                    break
+            else:
+                print('Meson: Warning: No flags found')
+
+            task.return_boolean(True)
+
+        thread = threading.Thread(target=build_flags_thread)
+        thread.start()
+
+    def do_get_build_flags_finish(self, result):
+        if result.propagate_boolean():
+            return result.build_flags
+        return []
+
+    def do_get_build_targets_async(self, cancellable, callback, data=None):
+        task = Gio.Task.new(self, cancellable, callback)
+        task.build_targets = []
+
+        # TODO: Cleaner API for this?
+        config = self._cached_config
+        builder = self._cached_builder
+
+        def build_targets_thread():
+            # TODO: Ide.Subprocess.communicate_utf8(None, cancellable) doesn't work?
+            try:
+                ret = subprocess.check_output(['mesonintrospect', '--targets',
+                                               builder._get_build_dir().get_path()])
+            except (subprocess.CalledProcessError, FileNotFoundError):
+                task.return_error(GLib.Error('Failed to run mesonintrospect'))
+                task.return_boolean(False)
+                return
+
+            targets = []
+            try:
+                meson_targets = json.loads(ret.decode('utf-8'))
+            except json.JSONDecodeError:
+                task.return_error(GLib.Error('Failed to decode meson json'))
+                task.return_boolean(False)
+                return
+
+            bindir = path.join(config.get_prefix(), 'bin')
+            for t in meson_targets:
+                name = t['filename']
+                if isinstance(name, list):
+                    name = name[0]
+
+                install_dir = t.get('install_filename', '')
+                installed = t['installed']
+                if installed and not install_dir:
+                    print('Meson: Warning: Older versions of Meson did not expose install dir')
+                    if t['type'] == 'executable':
+                        # Hardcode bad guess
+                        install_dir = bindir
+                elif install_dir:
+                    install_dir = path.dirname(install_dir)
+
+                ide_target = MesonBuildTarget(install_dir, name=name)
+                # Try to be smart and sort these because Builder runs the
+                # first one. Ideally it allows the user to select the run targets.
+                if t['type'] == 'executable' and installed and \
+                    install_dir.startswith(bindir) and not t['filename'].endswith('-cli'):
+                    targets.insert(0, ide_target)
+                else:
+                    targets.append(ide_target)
+
+            task.build_targets = targets
+            task.return_boolean(True)
+
+        thread = threading.Thread(target=build_targets_thread)
+        thread.start()
+
+    def do_get_build_targets_finish(self, result):
+        if result.propagate_boolean():
+            return result.build_targets
+
+
+class MesonBuilder(Ide.Builder):
+    configuration = GObject.Property(type=Ide.Configuration)
+
+    def __init__(self, **kwargs):
+        super().__init__(**kwargs)
+
+    def _get_build_dir(self) -> Gio.File:
+        context = self.get_context()
+
+        # This matches the Autotools layout
+        project_id = context.get_project().get_id()
+        buildroot = context.get_root_build_dir()
+        device = self.props.configuration.get_device()
+        device_id = device.get_id()
+        system_type = device.get_system_type()
+
+        return Gio.File.new_for_path(path.join(buildroot, project_id, device_id, system_type))
+
+    def _get_source_dir(self) -> Gio.File:
+        context = self.get_context()
+        return context.get_vcs().get_working_directory()
+
+    def do_build_async(self, flags, cancellable, callback, data=None):
+        task = Gio.Task.new(self, cancellable, callback)
+        task.build_result = MesonBuildResult(self.configuration,
+                                             self._get_build_dir(),
+                                             self._get_source_dir(),
+                                             cancellable,
+                                             flags=flags)
+
+        def wrap_build():
+            task.build_result.set_running(True)
+            try:
+                task.build_result.build()
+                task.build_result.set_mode(_('Successful'))
+                task.build_result.set_failed(False)
+                task.return_boolean(True)
+            except GLib.Error as e:
+                task.build_result.set_mode(_('Failed'))
+                task.build_result.set_failed(True)
+                task.return_error(e)
+            task.build_result.set_running(False)
+
+        thread = threading.Thread(target=wrap_build)
+        thread.start()
+
+        return task.build_result
+
+    def do_build_finish(self, result) -> Ide.BuildResult:
+        if result.propagate_boolean():
+            return result.build_result
+
+    def do_install_async(self, cancellable, callback, data=None):
+        task = Gio.Task.new(self, cancellable, callback)
+        task.build_result = MesonBuildResult(self.configuration,
+                                             self._get_build_dir(),
+                                             self._get_source_dir(),
+                                             cancellable)
+
+        def wrap_install():
+            task.build_result.set_running(True)
+            try:
+                task.build_result.install()
+                self = task.get_source_object()
+                task.build_result.set_mode(_('Successful'))
+                task.build_result.set_failed(False)
+                task.return_boolean(True)
+            except GLib.Error as e:
+                task.build_result.set_mode(_("Failed"))
+                task.build_result.set_failed(True)
+                task.return_error(e)
+            task.build_result.set_running(False)
+
+        thread = threading.Thread(target=wrap_install)
+        thread.start()
+
+        return task.build_result
+
+    def do_install_finish(self, result) -> Ide.BuildResult:
+        if result.propagate_boolean():
+            return result.build_result
+
+
+class MesonBuildResult(Ide.BuildResult):
+
+    def __init__(self, config, blddir, srcdir, cancel, flags=0, **kwargs):
+        super().__init__(**kwargs)
+        self.config = config
+        self.cancel = cancel
+        self.flags = flags
+        self.runtime = config.get_runtime()
+        self.blddir = blddir
+        self.srcdir = srcdir
+
+    def _new_launcher(self, cwd=None):
+        if self.runtime:
+            launcher = self.runtime.create_launcher()
+        else:
+            launcher = Ide.SubprocessLauncher.new(Gio.SubprocessFlags.NONE)
+            launcher.set_run_on_host(True)
+            launcher.set_clear_env(False)
+        if cwd:
+            launcher.set_cwd(cwd.get_path())
+        return launcher
+
+    def _get_ninja(self):
+        if not ninja:
+            global ninja
+            ninja = GLib.find_program_in_path('ninja-build')
+            if not ninja:
+                ninja = GLib.find_program_in_path('ninja')
+            if not ninja:
+                ninja = 'ninja'
+        return ninja
+
+    def _run_subprocess(self, launcher):
+        self.log_stdout_literal('Running: {}…'.format(' '.join(launcher.get_argv())))
+        proc = launcher.spawn()
+        self.log_subprocess(proc)
+        proc.wait_check(self.cancel)
+
+    def install(self):
+        launcher = self._new_launcher(cwd=self.blddir)
+        launcher.push_args([self._get_ninja(), 'install'])
+        self._run_subprocess(launcher)
+
+    def build(self):
+        """
+        NOTE: This is ran in a thread and it raising GLib.Error is handled a layer up.
+        """
+        clean = bool(self.flags & Ide.BuilderBuildFlags.FORCE_CLEAN)
+        build = not self.flags & Ide.BuilderBuildFlags.NO_BUILD
+        bootstrap = bool(self.flags & Ide.BuilderBuildFlags.FORCE_BOOTSTRAP)
+
+        self.log_stdout_literal('Starting Build…')
+        if bootstrap or self.config.get_dirty():
+            self.log_stdout_literal('Deleting build directory…')
+            try:
+                shutil.rmtree(self.blddir.get_path())
+            except FileNotFoundError:
+                pass
+            self.config.set_dirty(False)
+
+        if not self.blddir.query_exists():
+            self.log_stdout_literal('Creating build directory…')
+            self.blddir.make_directory_with_parents(self.cancel)
+
+        # TODO: For dirty config we could use `mesonconf` but it does not
+        # handle removing defines which might be unclear
+
+        if not self.blddir.get_child('build.ninja').query_exists():
+            self.log_stdout_literal('Running Meson…')
+            config_opts = self.config.get_config_opts()
+            extra_opts = config_opts.split() if config_opts else []
+            extra_opts.append('--prefix=' + self.config.get_prefix())
+            launcher = self._new_launcher(self.srcdir)
+            launcher.push_args(['meson', self.blddir.get_path()] + extra_opts)
+
+            self.set_mode(_('Configuring…'))
+            self._run_subprocess(launcher)
+
+        launcher = self._new_launcher(self.blddir)
+        launcher.push_args([self._get_ninja()])
+        if clean:
+            self.log_stdout_literal('Cleaning…')
+            self.set_mode(_('Cleaning…'))
+            launcher.push_args(['clean'])
+            self._run_subprocess(launcher)
+        if build:
+            if clean: # Build after cleaning
+                launcher.pop_argv()
+            self.log_stdout_literal('Building…')
+            self.set_mode(_('Building…'))
+            self._run_subprocess(launcher)
+
+class MesonBuildTarget(Ide.Object, Ide.BuildTarget):
+    # FIXME: These should be part of the BuildTarget interface
+    name = GObject.Property(type=str)
+    install_directory = GObject.Property(type=Gio.File)
+
+    def __init__(self, install_dir, **kwargs):
+        super().__init__(**kwargs)
+        self.props.install_directory = Gio.File.new_for_path(install_dir)
+
+    def do_get_install_directory(self):
+        return self.props.install_directory
+


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]