Repository: mesonbuild/meson

Issue:
yelp support
To make it easy to port GNOME modules from autotools to meson, it'll be nice to have some convenience from meson for building/installing user help pages.



Discussion:
I'll be needing this soon on my port of gnome-builder so I'll look into it.

For reference, here's the M4 file that provides the autotools build rules we use:

https://git.gnome.org/browse/yelp-tools/tree/tools/yelp.m4

And here's some docs on how to use it:

http://yelp.io/tools/yelp.m4

Happy to help out on this if someone can give me some pointers.

I think we would largely be duplicating the logic the [gnome.gtkdoc()](https://github.com/mesonbuild/meson/blob/b6b8d561b84eb950d1503e682ca2eab3b656efa9/mesonbuild/modules/gnome.py#L369-L418) function uses which has an install helper [gtkdochelper.py](https://github.com/mesonbuild/meson/blob/b6b8d561b84eb950d1503e682ca2eab3b656efa9/mesonbuild/scripts/gtkdochelper.py).

On top of that it doesn't integrate with normal translations so either we could reuse the [gettext.py](https://github.com/mesonbuild/meson/blob/b6b8d561b84eb950d1503e682ca2eab3b656efa9/mesonbuild/scripts/gettext.py) install helper or just duplicate part of that. Might have to expose a few extra targets to help manage these.

The api for it should be fairly simple like:

``` python
gnome.yelp('your-id',
   sources: ['index.page', 'something.page'],
   media: ['something.png'],
   languages: ['es', 'de'],
   install: true,
)
```


Resolving patch:
diff --git a/mesonbuild/mesonmain.py b/mesonbuild/mesonmain.py
--- a/mesonbuild/mesonmain.py
+++ b/mesonbuild/mesonmain.py
@@ -227,6 +227,9 @@ def run_script_command(args):
     elif cmdname == 'gettext':
         import mesonbuild.scripts.gettext as abc
         cmdfunc = abc.run
+    elif cmdname == 'yelphelper':
+        import mesonbuild.scripts.yelphelper as abc
+        cmdfunc = abc.run
     else:
         raise MesonException('Unknown internal command {}.'.format(cmdname))
     return cmdfunc(cmdargs)
diff --git a/mesonbuild/modules/gnome.py b/mesonbuild/modules/gnome.py
--- a/mesonbuild/modules/gnome.py
+++ b/mesonbuild/modules/gnome.py
@@ -17,6 +17,7 @@
 
 from .. import build
 import os
+import sys
 import subprocess
 from ..mesonlib import MesonException
 from .. import dependencies
@@ -452,6 +453,65 @@ def compile_schemas(self, state, args, kwargs):
         target_g = build.CustomTarget(targetname, state.subdir, kwargs)
         return target_g
 
+    def yelp(self, state, args, kwargs):
+        if len(args) < 1:
+            raise MesonException('Yelp requires a project id')
+
+        project_id = args[0]
+        sources = mesonlib.stringlistify(kwargs.pop('sources', []))
+        if not sources:
+            if len(args) > 1:
+                sources = mesonlib.stringlistify(args[1:])
+            if not sources:
+                raise MesonException('Yelp requires a list of sources')
+        source_str = '@@'.join(sources)
+
+        langs = mesonlib.stringlistify(kwargs.pop('languages', []))
+        media = mesonlib.stringlistify(kwargs.pop('media', []))
+        symlinks = kwargs.pop('symlink_media', False)
+
+        if not isinstance(symlinks, bool):
+            raise MesonException('symlink_media must be a boolean')
+
+        if kwargs:
+            raise MesonException('Unknown arguments passed: {}'.format(', '.join(kwargs.keys())))
+
+        install_cmd = [
+            sys.executable,
+            state.environment.get_build_command(),
+            '--internal',
+            'yelphelper',
+            'install',
+            '--subdir=' + state.subdir,
+            '--id=' + project_id,
+            '--installdir=' + os.path.join(state.environment.get_datadir(), 'help'),
+            '--sources=' + source_str,
+        ]
+        if symlinks:
+            install_cmd.append('--symlinks=true')
+        if media:
+            install_cmd.append('--media=' + '@@'.join(media))
+        if langs:
+            install_cmd.append('--langs=' + '@@'.join(langs))
+        inscript = build.InstallScript(install_cmd)
+
+        potargs = [state.environment.get_build_command(), '--internal', 'yelphelper', 'pot',
+                   '--subdir=' + state.subdir,
+                   '--id=' + project_id,
+                   '--sources=' + source_str]
+        pottarget = build.RunTarget('help-' + project_id + '-pot', sys.executable,
+                                     potargs, [], state.subdir)
+
+        poargs = [state.environment.get_build_command(), '--internal', 'yelphelper', 'update-po',
+                   '--subdir=' + state.subdir,
+                   '--id=' + project_id,
+                   '--sources=' + source_str,
+                   '--langs=' + '@@'.join(langs)]
+        potarget = build.RunTarget('help-' + project_id + '-update-po', sys.executable,
+                                     poargs, [], state.subdir)
+
+        return [inscript, pottarget, potarget]
+
     def gtkdoc(self, state, args, kwargs):
         if len(args) != 1:
             raise MesonException('Gtkdoc must have one positional argument.')
diff --git a/mesonbuild/scripts/yelphelper.py b/mesonbuild/scripts/yelphelper.py
new file mode 100644
--- /dev/null
+++ b/mesonbuild/scripts/yelphelper.py
@@ -0,0 +1,118 @@
+# Copyright 2016 Patrick Griffis <tingping@tingping.se>
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import sys, os
+import subprocess
+import shutil
+import argparse
+from mesonbuild import mlog
+from mesonbuild.mesonlib import MesonException
+from mesonbuild.scripts import destdir_join
+
+parser = argparse.ArgumentParser()
+parser.add_argument('command')
+parser.add_argument('--id', dest='project_id')
+parser.add_argument('--subdir', dest='subdir')
+parser.add_argument('--installdir', dest='install_dir')
+parser.add_argument('--sources', dest='sources')
+parser.add_argument('--media', dest='media', default='')
+parser.add_argument('--langs', dest='langs', default='')
+parser.add_argument('--symlinks', type=bool, dest='symlinks', default=False)
+
+def build_pot(srcdir, project_id, sources):
+    # Must be relative paths
+    sources = [os.path.join('C', source) for source in sources]
+    outfile = os.path.join(srcdir, project_id + '.pot')
+    subprocess.call(['itstool', '-o', outfile, *sources])
+
+def update_po(srcdir, project_id, langs):
+    potfile = os.path.join(srcdir, project_id + '.pot')
+    for lang in langs:
+        pofile = os.path.join(srcdir, lang, lang + '.po')
+        subprocess.call(['msgmerge', '-q', '-o', pofile, pofile, potfile])
+
+def build_translations(srcdir, blddir, langs):
+    for lang in langs:
+        outdir = os.path.join(blddir, lang)
+        os.makedirs(outdir, exist_ok=True)
+        subprocess.call([
+            'msgfmt', os.path.join(srcdir, lang, lang + '.po'),
+            '-o', os.path.join(outdir, lang + '.gmo')
+        ])
+
+def merge_translations(blddir, sources, langs):
+    for lang in langs:
+        subprocess.call([
+            'itstool', '-m', os.path.join(blddir, lang, lang + '.gmo'),
+            '-o', os.path.join(blddir, lang),
+            *sources,
+        ])
+
+def install_help(srcdir, blddir, sources, media, langs, install_dir, destdir, project_id, symlinks):
+    c_install_dir = os.path.join(install_dir, 'C', project_id)
+    for lang in langs + ['C']:
+        indir = destdir_join(destdir, os.path.join(install_dir, lang, project_id))
+        os.makedirs(indir, exist_ok=True)
+        for source in sources:
+            infile = os.path.join(srcdir if lang == 'C' else blddir, lang, source)
+            outfile = os.path.join(indir, source)
+            mlog.log('Installing %s to %s.' %(infile, outfile))
+            shutil.copyfile(infile, outfile)
+            shutil.copystat(infile, outfile)
+        for m in media:
+            infile = os.path.join(srcdir, lang, m)
+            outfile = os.path.join(indir, m)
+            if not os.path.exists(infile):
+                if lang == 'C':
+                    mlog.log(mlog.bold('Warning:'), 'Media file "%s" did not exist in C directory' %m)
+                elif symlinks:
+                    srcfile = os.path.join(c_install_dir, m)
+                    mlog.log('Symlinking %s to %s.' %(outfile, srcfile))
+                    if '/' in m or '\\' in m:
+                        os.makedirs(os.path.dirname(outfile), exist_ok=True)
+                    os.symlink(srcfile, outfile)
+                continue
+            symfile = os.path.join(install_dir, m)
+            mlog.log('Installing %s to %s.' %(infile, outfile))
+            if '/' in m or '\\' in m:
+                os.makedirs(os.path.dirname(outfile), exist_ok=True)
+            shutil.copyfile(infile, outfile)
+            shutil.copystat(infile, outfile)
+
+def run(args):
+    options = parser.parse_args(args)
+    langs = options.langs.split('@@') if options.langs else []
+    media = options.media.split('@@') if options.media else []
+    sources = options.sources.split('@@')
+    destdir = os.environ.get('DESTDIR', '')
+    src_subdir = os.path.join(os.environ['MESON_SOURCE_ROOT'], options.subdir)
+    build_subdir = os.path.join(os.environ['MESON_BUILD_ROOT'], options.subdir)
+    abs_sources = [os.path.join(src_subdir, 'C', source) for source in sources]
+
+    if options.command == 'pot':
+        build_pot(src_subdir, options.project_id, sources)
+    elif options.command == 'update-po':
+        build_pot(src_subdir, options.project_id, sources)
+        update_po(src_subdir, options.project_id, langs)
+    elif options.command == 'build':
+        if langs:
+            build_translations(src_subdir, build_subdir, langs)
+    elif options.command == 'install':
+        install_dir = os.path.join(os.environ['MESON_INSTALL_PREFIX'], options.install_dir)
+        if langs:
+            build_translations(src_subdir, build_subdir, langs)
+            merge_translations(build_subdir, abs_sources, langs)
+        install_help(src_subdir, build_subdir, sources, media, langs, install_dir,
+                     destdir, options.project_id, options.symlinks)
+


Test patch:
diff --git a/test cases/frameworks/13 yelp/help/C/index.page b/test cases/frameworks/13 yelp/help/C/index.page
new file mode 100644
--- /dev/null
+++ b/test cases/frameworks/13 yelp/help/C/index.page	
@@ -0,0 +1,8 @@
+<page xmlns="http://projectmallard.org/1.0/"
+      xmlns:its="http://www.w3.org/2005/11/its"
+      type="guide"
+      id="index">
+  <title>
+        Hello!
+  </title>
+</page>
diff --git a/test cases/frameworks/13 yelp/help/C/media/test.txt b/test cases/frameworks/13 yelp/help/C/media/test.txt
new file mode 100644
--- /dev/null
+++ b/test cases/frameworks/13 yelp/help/C/media/test.txt	
@@ -0,0 +1 @@
+hello
diff --git a/test cases/frameworks/13 yelp/help/de/de.po b/test cases/frameworks/13 yelp/help/de/de.po
new file mode 100644
--- /dev/null
+++ b/test cases/frameworks/13 yelp/help/de/de.po	
@@ -0,0 +1,13 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: meson master\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. (itstool) path: page/title
+#: C/index.page:5
+msgid "Hello!"
+msgstr "Hallo!"
diff --git a/test cases/frameworks/13 yelp/help/es/es.po b/test cases/frameworks/13 yelp/help/es/es.po
new file mode 100644
--- /dev/null
+++ b/test cases/frameworks/13 yelp/help/es/es.po	
@@ -0,0 +1,13 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: meson master\n"
+"Language: es\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. (itstool) path: page/title
+#: C/index.page:5
+msgid "Hello!"
+msgstr "¡Hola!"
diff --git a/test cases/frameworks/13 yelp/help/es/media/test.txt b/test cases/frameworks/13 yelp/help/es/media/test.txt
new file mode 100644
--- /dev/null
+++ b/test cases/frameworks/13 yelp/help/es/media/test.txt	
@@ -0,0 +1 @@
+Hola.
diff --git a/test cases/frameworks/13 yelp/help/meson.build b/test cases/frameworks/13 yelp/help/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/frameworks/13 yelp/help/meson.build	
@@ -0,0 +1,7 @@
+gnome = import('gnome')
+
+gnome.yelp('meson',
+   sources: 'index.page',
+   media: 'media/test.txt',
+   languages: ['de', 'es'],
+)
diff --git a/test cases/frameworks/13 yelp/installed_files.txt b/test cases/frameworks/13 yelp/installed_files.txt
new file mode 100644
--- /dev/null
+++ b/test cases/frameworks/13 yelp/installed_files.txt	
@@ -0,0 +1,5 @@
+usr/share/help/C/meson/index.page
+usr/share/help/C/meson/media/test.txt
+usr/share/help/es/meson/index.page
+usr/share/help/es/meson/media/test.txt
+usr/share/help/de/meson/index.page
diff --git a/test cases/frameworks/13 yelp/meson.build b/test cases/frameworks/13 yelp/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/frameworks/13 yelp/meson.build	
@@ -0,0 +1,2 @@
+project('yelp', 'c')
+subdir('help')