From a442b86b7d05628b00b6895a09a37b5cc0dc6be1 Mon Sep 17 00:00:00 2001
From: Jan Tojnar <jtojnar@gmail.com>
Date: Sun, 4 Feb 2024 04:23:47 +0100
Subject: [PATCH 1/3] build: Substitute doc configs

Since Meson uses out-of-tree builds, we need to make the paths
in the config replaceable before we can switch to Meson.
---
 configure.ac                       | 7 +++++++
 doc/.gitignore                     | 1 +
 doc/Doxyfile.in                    | 6 +++---
 doc/Makefile.am                    | 1 -
 doc/source/{conf.py => conf.py.in} | 6 +++---
 5 files changed, 14 insertions(+), 7 deletions(-)
 rename doc/source/{conf.py => conf.py.in} (98%)

diff --git a/configure.ac b/configure.ac
index b6e1e060..85d2e740 100644
--- a/configure.ac
+++ b/configure.ac
@@ -296,12 +296,19 @@ fi
 AM_CONDITIONAL(ENABLE_GEGL, test "x$enable_gegl" = "xyes")
 AC_SUBST(DOXYGEN_EXCLUDED)
 
+DOXYXML_BUILD_PATH="$PWD/doc"
+AC_SUBST(DOXYXML_BUILD_PATH)
+
+DOXYGEN_SOURCE_ROOT="$PWD"
+AC_SUBST(DOXYGEN_SOURCE_ROOT)
+
 # Set pkg-config variables before generation.
 AC_SUBST(PKG_CONFIG_REQUIRES)
 
 AC_CONFIG_FILES([
   doc/Doxyfile
   doc/Makefile
+  doc/source/conf.py
   gegl/libmypaint-gegl-]libmypaint_api_platform_version()[.pc:gegl/libmypaint-gegl.pc.in
   gegl/Makefile
   libmypaint-]libmypaint_api_platform_version()[.pc:libmypaint.pc.in
diff --git a/doc/.gitignore b/doc/.gitignore
index e88d994a..8cb71b38 100644
--- a/doc/.gitignore
+++ b/doc/.gitignore
@@ -1,3 +1,4 @@
+source/conf.py
 Doxyfile
 doxygen/
 build/
diff --git a/doc/Doxyfile.in b/doc/Doxyfile.in
index e48274ef..628e2e96 100644
--- a/doc/Doxyfile.in
+++ b/doc/Doxyfile.in
@@ -38,7 +38,7 @@ PROJECT_NAME           = "libmypaint"
 # could be handy for archiving the generated documentation or if some version
 # control system is used.
 
-PROJECT_NUMBER         = 0.1
+PROJECT_NUMBER         = @LIBMYPAINT_VERSION@
 
 # Using the PROJECT_BRIEF tag one can provide an optional one line description
 # for a project that appears at the top of each page and should give viewer a
@@ -58,7 +58,7 @@ PROJECT_LOGO           =
 # entered, it will be relative to the location where doxygen was started. If
 # left blank the current directory will be used.
 
-OUTPUT_DIRECTORY       =
+OUTPUT_DIRECTORY       = @DOXYXML_BUILD_PATH@
 
 # If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub-
 # directories (in 2 levels) under the output directory of each output format and
@@ -819,7 +819,7 @@ WARN_LOGFILE           =
 # spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
 # Note: If this tag is empty the current directory is searched.
 
-INPUT                  = ../..
+INPUT                  = @DOXYGEN_SOURCE_ROOT@
 
 # This tag can be used to specify the character encoding of the source files
 # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
diff --git a/doc/Makefile.am b/doc/Makefile.am
index eb21386a..5b994fbd 100644
--- a/doc/Makefile.am
+++ b/doc/Makefile.am
@@ -1,5 +1,4 @@
 EXTRA_DIST =		\
-	source/conf.py	\
 	source/index.rst
 
 if ENABLE_DOCS
diff --git a/doc/source/conf.py b/doc/source/conf.py.in
similarity index 98%
rename from doc/source/conf.py
rename to doc/source/conf.py.in
index ee1b12f4..5b2f1ae8 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py.in
@@ -37,7 +37,7 @@
 
 # Breathe setup, for integrating doxygen content
 extensions.append('breathe')
-doxyxml_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../doxygen')
+doxyxml_dir = '@DOXYXML_BUILD_PATH@'
 print(doxyxml_dir)
 breathe_projects = {"libmypaint": doxyxml_dir}
 breathe_default_project = "libmypaint"
@@ -63,9 +63,9 @@
 # built documents.
 #
 # The short X.Y version.
-version = '0.1'
+version = '@LIBMYPAINT_VERSION@'
 # The full version, including alpha/beta/rc tags.
-release = '0.1'
+release = '@LIBMYPAINT_VERSION_FULL@'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.

From 021ecc2bc17ff4a0f6e4a88750eff80144e730ad Mon Sep 17 00:00:00 2001
From: Elliott Sales de Andrade <quantum.analyst@gmail.com>
Date: Mon, 9 Apr 2018 00:23:15 -0400
Subject: [PATCH 2/3] Add Meson build system
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: Jan Tojnar <jtojnar@gmail.com>
Co-authored-by: Félix Piédallu <felix@piedallu.me>
---
 .github/workflows/test.yaml  |  36 +++-
 appveyor_build.sh            |   9 +-
 build-aux/fix-po-location.py |  21 +++
 doc/meson.build              |  37 ++++
 doc/source/meson.build       |   5 +
 gegl/meson.build             |  66 +++++++
 meson.build                  | 331 +++++++++++++++++++++++++++++++++++
 meson_options.txt            |  50 ++++++
 po/meson.build               | 160 +++++++++++++++++
 tests/meson.build            |  92 ++++++++++
 10 files changed, 804 insertions(+), 3 deletions(-)
 create mode 100755 build-aux/fix-po-location.py
 create mode 100644 doc/meson.build
 create mode 100644 doc/source/meson.build
 create mode 100644 gegl/meson.build
 create mode 100644 meson.build
 create mode 100644 meson_options.txt
 create mode 100644 po/meson.build
 create mode 100644 tests/meson.build

diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 80b3b48a..5ff73b33 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -5,8 +5,8 @@ on:
     - pull_request
 
 jobs:
-    linux:
-        name: Linux
+    linux-autotools:
+        name: Linux Autotools
         runs-on: ubuntu-22.04
         strategy:
             matrix:
@@ -35,3 +35,35 @@ jobs:
                 make
             - name: "Run tests"
               run: make distcheck
+
+    linux-meson:
+        name: Linux Meson
+        runs-on: ubuntu-22.04
+        strategy:
+            matrix:
+                configureFlags:
+                    - ""
+                    - "-Dglib=enabled -Dintrospection=enabled"
+                    - "-Dgegl=enabled"
+                include:
+                    - configureFlags: "-Dglib=enabled -Dintrospection=enabled"
+                      extraDeps: "libgirepository1.0-dev"
+                    - configureFlags: "-Dgegl=enabled"
+                      extraDeps: "libgegl-dev"
+        steps:
+            - uses: actions/checkout@v4
+            - name: "Install dependencies"
+              run: |
+                sudo apt-get update
+                sudo apt-get install -y \
+                    libjson-c-dev \
+                    meson \
+                    ninja-build \
+                    gettext \
+                    ${{ matrix.extraDeps }}
+            - name: "Build"
+              run: |
+                meson setup _build --buildtype=release -Dauto_features=disabled
+                meson compile -C _build
+            - name: "Run tests"
+              run: meson dist -C _build
diff --git a/appveyor_build.sh b/appveyor_build.sh
index 88c15a9b..6d61fd00 100644
--- a/appveyor_build.sh
+++ b/appveyor_build.sh
@@ -11,7 +11,9 @@ pacman --noconfirm -S --needed \
        base-devel \
        ${PKG_PREFIX}-json-c \
        ${PKG_PREFIX}-glib2 \
-       ${PKG_PREFIX}-gobject-introspection
+       ${PKG_PREFIX}-gobject-introspection \
+       ${PKG_PREFIX}-meson \
+       git
 
 
 # Add m4 directories to the ACLOCAL_PATH
@@ -30,6 +32,11 @@ done
 export ACLOCAL_PATH
 export PWD="$APPVEYOR_BULD_FOLDER"
 
+# Check that Meson build works.
+meson setup _build --buildtype=release -Dgegl=false -Ddocs=false
+meson dist -C _build
+rm -rf _build
+
 ./autogen.sh
 ./configure
 make distcheck
diff --git a/build-aux/fix-po-location.py b/build-aux/fix-po-location.py
new file mode 100755
index 00000000..8d649723
--- /dev/null
+++ b/build-aux/fix-po-location.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python3
+
+"""
+The `generate.py` script will pass location info as a translator note.
+This script converts it to a proper location comment.
+"""
+
+import re
+import sys
+
+LOCATION_PATTERN = re.compile(r"^#\. (: \.\./brushsettings.json:.*)", re.MULTILINE)
+
+match sys.argv:
+    case [_, input_path, output_path]:
+        pass
+    case _:
+        print("usage: fix-po-location.py <input> <output>", file=sys.stderr)
+        sys.exit(1)
+
+with open(input_path) as po_in, open(output_path, "w") as po_out:
+    po_out.write(LOCATION_PATTERN.sub(r"#\1", po_in.read()))
diff --git a/doc/meson.build b/doc/meson.build
new file mode 100644
index 00000000..54cb3c65
--- /dev/null
+++ b/doc/meson.build
@@ -0,0 +1,37 @@
+doc_conf = configuration_data()
+doc_conf.merge_from(conf)
+
+doc_conf.set('DOXYGEN_SOURCE_ROOT', meson.project_source_root())
+doc_conf.set('DOXYXML_BUILD_PATH', meson.current_build_dir())
+doc_conf.set('DOXYGEN_EXCLUDED', '')
+
+doxyfile = configure_file(
+  input: 'Doxyfile.in',
+  output: 'Doxyfile',
+  configuration: doc_conf,
+)
+
+doxygen_index = custom_target(
+  'doxygen',
+  input: doxyfile,
+  output: 'index.xml',
+  command: [
+    doxygen,
+    '@INPUT@',
+  ],
+)
+
+subdir('source')
+
+run_target(
+  'sphinx',
+  depends: [
+    doxygen_index,
+  ],
+  command: [
+    sphinx_build,
+    '-c', meson.current_build_dir() / 'source',
+    meson.current_source_dir() / 'source',
+    meson.current_build_dir() / 'build',
+  ],
+)
diff --git a/doc/source/meson.build b/doc/source/meson.build
new file mode 100644
index 00000000..d21b005c
--- /dev/null
+++ b/doc/source/meson.build
@@ -0,0 +1,5 @@
+sphinx_conf_file = configure_file(
+  input: 'conf.py.in',
+  output: 'conf.py',
+  configuration: doc_conf,
+)
diff --git a/gegl/meson.build b/gegl/meson.build
new file mode 100644
index 00000000..fc4adc31
--- /dev/null
+++ b/gegl/meson.build
@@ -0,0 +1,66 @@
+libmypaint_gegl_inc = include_directories('.')
+
+libmypaint_gegl_sources = [
+  '../glib/mypaint-gegl-glib.c',
+  'mypaint-gegl-surface.c',
+]
+
+libmypaint_gegl_headers = [
+  '../glib/mypaint-gegl-glib.h',
+  'mypaint-gegl-surface.h',
+]
+
+libmypaint_gegl = library(
+  f'mypaint-gegl-@api_platform_version@',
+  libmypaint_gegl_sources,
+  include_directories: toplevel_inc,
+  link_with: libmypaint,
+  dependencies: [
+    json,
+    gobject,
+    gegl,
+  ],
+  version: abi_version_info,
+  install: true,
+)
+
+install_headers(
+  libmypaint_gegl_headers,
+  subdir: 'libmypaint-gegl',
+)
+
+
+if use_introspection
+  gnome = import('gnome')
+
+  libmypaint_gegl_gir = gnome.generate_gir(
+    libmypaint_gegl,
+    namespace: 'MyPaintGegl',
+    nsversion: api_platform_version,
+
+    sources: libmypaint_gegl_sources + libmypaint_gegl_headers,
+    symbol_prefix: 'mypaint_gegl',
+    identifier_prefix: 'MyPaintGegl',
+
+    includes: [
+      'GObject-2.0',
+      gegl_gir,
+      libmypaint_gir[0],
+    ],
+    install: true,
+  )
+endif
+
+
+pkgconfig.generate(
+  libmypaint_gegl,
+  name: meson.project_name() + '-gegl-' + api_platform_version,
+  version: version_full,
+  description: 'MyPaint brush engine library, with GEGL integration',
+  requires: [
+    libmypaint,
+    gegl,
+  ],
+  url: project_url,
+  subdirs: 'libmypaint-gegl',
+)
diff --git a/meson.build b/meson.build
new file mode 100644
index 00000000..282a8da0
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,331 @@
+project(
+  'libmypaint',
+  'c',
+  # API version: see https://github.com/mypaint/libmypaint/wiki/Versioning
+  # See http://semver.org/ for what this means.
+  version: '2.0.0-beta',
+  meson_version: '>=0.60.0',
+  default_options: [
+    'c_std=c99',
+  ],
+)
+
+cc = meson.get_compiler('c')
+
+conf = configuration_data()
+
+pkgconfig = import('pkgconfig')
+pymod = import('python')
+
+prefix = get_option('prefix')
+includedir = prefix / get_option('includedir')
+localedir = prefix / get_option('localedir')
+
+###############################################################################
+# Project information.
+
+version_full = meson.project_version()
+version_dash_split = version_full.split('-')
+version_stable = version_dash_split[0]
+
+version_array = version_stable.split('.')
+version_major = version_array[0]
+version_minor = version_array[1]
+version_micro = version_array[2]
+
+# The API "platform" or "intercompatibility" version.
+#
+# This one is used for library name prefixes, for introspection
+# namespace versions, for gettext domains, and basically anything that
+# needs to change when backwards or forwards API compatibility changes.
+# Another way of thinking about it: it allows meaningful side by side
+# installations of libmypaint.
+api_platform_version = f'@version_major@.@version_minor@'
+api_name = f'libmypaint-@api_platform_version@'
+
+project_url = 'https://github.com/mypaint/libmypaint'
+
+conf.set('PACKAGE_NAME', meson.project_name())
+conf.set('PACKAGE_URL', project_url)
+conf.set('LIBMYPAINT_API_PLATFORM_VERSION', api_platform_version)
+conf.set('LIBMYPAINT_VERSION', version_stable)
+conf.set('LIBMYPAINT_VERSION_FULL', version_full)
+
+gettext_package = api_name
+conf.set_quoted(
+  'GETTEXT_PACKAGE',
+  gettext_package,
+  description: 'The prefix for our gettext translation domains.',
+)
+
+###############################################################################
+# ABI version. Changes independently of API version.
+# See: https://autotools.io/libtool/version.html
+# https://www.gnu.org/software/libtool/manual/html_node/Updating-version-info.html
+# The rules are fiddly, and are summarized here.
+
+abi_current = 0 # inc when add/remove/change interfaces
+abi_revision = 0 # increment on every release
+abi_age = 0 # inc only if changes backward compat
+abi_soname_version = abi_current - abi_age
+abi_version_info = f'@abi_soname_version@.@abi_age@.@abi_revision@'
+
+###############################################################################
+# System detection, compiler options
+
+platform_win32 = (host_machine.system() == 'windows')
+platform_osx = (host_machine.system() == 'darwin')
+
+# Define strdup() in string.h under glibc >= 2.10 (POSIX.1-2008)
+add_project_arguments('-D_POSIX_C_SOURCE=200809L', language: 'c')
+
+###############################################################################
+# Dependencies
+
+libmath = cc.find_library('m', required: false)
+
+json = dependency('json-c')
+
+# glib
+gobject = dependency('gobject-2.0', required: get_option('glib'))
+use_glib = gobject.found()
+conf.set10('MYPAINT_CONFIG_USE_GLIB', use_glib)
+
+# GEGL
+gegl = dependency('gegl-0.4', 'gegl-0.3', required: get_option('gegl'))
+use_gegl = gegl.found()
+if use_gegl
+  gegl_gir = gegl.version().version_compare('>=0.4') ? 'Gegl-0.4' : 'Gegl-0.3'
+endif
+
+introspection_required_version = '1.32.0'
+introspection_feature = get_option(
+  'introspection',
+).require(
+  use_glib,
+  error_message: 'Generating GObject introspection requires building with GLib support',
+)
+# For g-ir-scanner and g-ir-compiler as used by the gnome module.
+gi = dependency(
+  'gobject-introspection-1.0',
+  version: f'>=@introspection_required_version@',
+  required: introspection_feature,
+  native: true,
+)
+use_introspection = gi.found()
+
+
+# OpenMP
+openmp = dependency('openmp', required: get_option('openmp'))
+
+## gperftools ##
+libprofiler = dependency('libprofiler', required: get_option('gperftools'))
+
+# Profiling
+if get_option('profiling')
+  add_project_arguments('-pg', language: 'c')
+endif
+
+# Internationalization
+cp = find_program('cp', required: false)
+msgfmt = find_program('msgfmt', required: false)
+msginit = find_program('msginit', required: false)
+msgmerge = find_program('msgmerge', required: false)
+mv = find_program('mv', required: false)
+xgettext = find_program('xgettext', required: false)
+fix_po_location = find_program('build-aux/fix-po-location.py')
+
+i18n_feature = get_option(
+  'i18n',
+).require(
+  msgfmt.found(),
+  error_message: 'I18n support requires msgfmt from gettext to build mo files',
+).require(
+  mv.found(),
+  error_message: 'I18n support requires mv to install mo files to proper location',
+)
+libintl = dependency('intl', required: i18n_feature)
+use_i18n = libintl.found()
+conf.set10('HAVE_GETTEXT', use_i18n)
+
+
+# Docs
+enable_docs = get_option('docs')
+if enable_docs
+  doxygen = find_program('doxygen')
+  sphinx_build = find_program(
+    'sphinx-build3',
+    'sphinx-build-3',
+    'sphinx-build2',
+    'sphinx-build-2',
+    'sphinx-build',
+  )
+
+  python3 = pymod.find_installation(
+    'python3',
+    modules: [
+      'breathe',
+    ],
+    required: true,
+  )
+
+  # todo: the python 'breathe' extension is also a dependency to doc building.
+  # the configure script should check for its existence.
+endif
+
+
+###############################################################################
+# Configure files
+
+toplevel_inc = include_directories('.')
+
+configure_file(
+  output: 'config.h',
+  configuration: conf,
+)
+
+brush_settings_headers = custom_target(
+  'brush_settings_headers',
+  input: 'brushsettings.json',
+  output: [
+    'mypaint-brush-settings-gen.h',
+    'brushsettings-gen.h',
+  ],
+  command: [
+    find_program('python3'),
+    meson.current_source_dir() / 'generate.py',
+    '@OUTPUT@',
+  ],
+  depend_files: [
+    'generate.py',
+  ],
+  install: true,
+  install_dir: [
+    includedir / api_name,
+    false,
+  ],
+)
+
+
+###############################################################################
+# Source files
+
+libmypaint_sources = [
+  'brushmodes.c',
+  'fifo.c',
+  'helpers.c',
+  'mypaint-brush-settings.c',
+  'mypaint-brush.c',
+  'mypaint-fixed-tiled-surface.c',
+  'mypaint-mapping.c',
+  'mypaint-matrix.c',
+  'mypaint-rectangle.c',
+  'mypaint-surface.c',
+  'mypaint-symmetry.c',
+  'mypaint-tiled-surface.c',
+  'mypaint.c',
+  'operationqueue.c',
+  'rng-double.c',
+  'tilemap.c',
+]
+
+libmypaint_introspectable_headers = [
+  'mypaint-brush.h',
+  'mypaint-brush-settings.h',
+  'mypaint-fixed-tiled-surface.h',
+  'mypaint-matrix.h',
+  'mypaint-rectangle.h',
+  'mypaint-surface.h',
+  'mypaint-symmetry.h',
+  'mypaint-tiled-surface.h',
+]
+
+libmypaint_public_headers = [
+  'mypaint-config.h',
+  'mypaint-glib-compat.h',
+  'mypaint-mapping.h',
+  libmypaint_introspectable_headers,
+]
+
+install_headers(
+  libmypaint_public_headers,
+  subdir: api_name,
+)
+
+# Install in subdirectory
+if use_glib
+  install_headers(
+    'glib/mypaint-brush.h',
+    subdir: api_name / 'glib',
+  )
+  libmypaint_introspectable_headers += 'glib/mypaint-brush.h'
+  libmypaint_public_headers += 'glib/mypaint-brush.h'
+endif
+
+# Do this after because you can't install_headers on a custom_target.
+libmypaint_introspectable_headers += brush_settings_headers[0]
+
+
+libmypaint = library(
+  f'mypaint-@api_platform_version@',
+  libmypaint_sources,
+  brush_settings_headers,
+  dependencies: [
+    gobject,
+    json,
+    libintl,
+    libmath,
+    openmp,
+  ],
+  version: abi_version_info,
+  install: true,
+)
+
+if use_introspection
+  gnome = import('gnome')
+
+  libmypaint_gir = gnome.generate_gir(
+    libmypaint,
+    nsversion: api_platform_version,
+    namespace: 'MyPaint',
+
+    sources: libmypaint_sources + libmypaint_introspectable_headers,
+    symbol_prefix: 'mypaint_',
+    identifier_prefix: 'MyPaint',
+
+    includes: [
+      'GLib-2.0',
+      'GObject-2.0',
+    ],
+    install: true,
+  )
+endif
+
+
+pkgconfig.generate(
+  libmypaint,
+  name: meson.project_name() + '-' + api_platform_version,
+  version: version_full,
+  description: 'MyPaint\'s brushstroke rendering library',
+  requires: [
+    json,
+    gobject,
+  ],
+  url: project_url,
+  subdirs: api_name,
+)
+
+
+if use_gegl
+  subdir('gegl')
+endif
+
+if use_i18n
+  subdir('po')
+endif
+
+subdir('tests')
+
+if enable_docs
+  subdir('doc')
+endif
diff --git a/meson_options.txt b/meson_options.txt
new file mode 100644
index 00000000..394187a9
--- /dev/null
+++ b/meson_options.txt
@@ -0,0 +1,50 @@
+option(
+  'glib',
+  type: 'feature',
+  value: 'auto',
+  description: 'Use GLib',
+)
+option(
+  'gegl',
+  type: 'feature',
+  value: 'auto',
+  description: 'Enable GEGL based code',
+)
+option(
+  'openmp',
+  type: 'feature',
+  value: 'auto',
+  description: 'Compile with OpenMP',
+)
+option(
+  'gperftools',
+  type: 'boolean',
+  value: false,
+  description: 'Enable gperftools in build, for profiling',
+)
+
+option(
+  'profiling',
+  type: 'boolean',
+  value: false,
+  description: 'Turn on profiling',
+)
+
+option(
+  'docs',
+  type: 'boolean',
+  value: false,
+  description: 'Enable documentation build',
+)
+option(
+  'i18n',
+  type: 'feature',
+  value: 'auto',
+  description: 'Enable internationalization',
+)
+option(
+  'introspection',
+  type: 'feature',
+  value: 'auto',
+  description: 'Enable GObject Instrospection (requires glib feature)',
+)
diff --git a/po/meson.build b/po/meson.build
new file mode 100644
index 00000000..737f6055
--- /dev/null
+++ b/po/meson.build
@@ -0,0 +1,160 @@
+# Translations need to be extracted from a generated file.  Unfortunately,
+# there is currently no way to express the dependency with `i18n.gettext()`,
+# so one would need to explicitly rebuild the project before any action.
+# https://github.com/mesonbuild/meson/issues/1733
+# Additionally, we need to do some post-processing because xgettext
+# would use the generated file path for location info.
+#
+# Let’s recreate the built-in targets created by `i18n.gettext()` ourselves.
+
+fs = import('fs')
+
+languages = fs.read('LINGUAS').split()
+
+gettext_maintainer_tools = {
+  'cp': cp.found(),
+  'msginit': msginit.found(),
+  'msgmerge': msgmerge.found(),
+  'xgettext': xgettext.found(),
+}
+
+has_maintainer_tools = true
+foreach tool, available : gettext_maintainer_tools
+  if not available
+    has_maintainer_tools = false
+    warning(tool + ' not found, maintainer targets will not work')
+  endif
+endforeach
+
+# Generate compiled message catalogues (.mo files).
+foreach lang : languages
+  message_dir = localedir / lang / 'LC_MESSAGES'
+  mo_lang_name = f'@gettext_package@-@lang@.mo'
+
+  custom_target(
+    mo_lang_name,
+    input: f'@lang@.po',
+    # This should really be `@gettext_package@.mo` for every language,
+    # and the languages should be distinguished by directory name
+    # but Meson does not support output in a subdirectory.
+    # https://github.com/mesonbuild/meson/issues/2320
+    output: mo_lang_name,
+    command: [
+      msgfmt,
+      '--output-file', '@OUTPUT@',
+      '@INPUT@',
+    ],
+    install: true,
+    install_dir: message_dir,
+    install_tag: 'i18n',
+  )
+
+  # Since we have to use different names to disambiguate and there is no
+  # `rename` kwarg, we need to rename it after installation.
+  # This will break uninstall script but 🤷‍♀️
+  meson.add_install_script(
+    mv,
+    message_dir / mo_lang_name,
+    message_dir / f'@gettext_package@.mo',
+  )
+endforeach
+
+if has_maintainer_tools
+  update_po_targets = []
+
+  # Update PO template.
+
+  pot = custom_target(
+    '_update-pot',
+    input: brush_settings_headers[1],
+    depends: [
+      brush_settings_headers,
+    ],
+    command: [
+      xgettext,
+      '--package-name=' + gettext_package,
+      '--output-dir=' + meson.project_build_root(),
+      '--directory=' + meson.project_build_root(),
+      '--output=@OUTPUT@',
+      '--add-comments',
+      '--keyword=N_:1',
+      '@INPUT@',
+      # The input is a generated file and our generator includes
+      # the actual source location in a comment for translators.
+      # We are going to convert it into a proper location comment
+      # in the next step.
+      '--no-location',
+    ],
+    output: 'libmypaint.pot.in',
+  )
+
+  pot = custom_target(
+    '_fix-pot',
+    input: pot,
+    command: [
+      # ...transform special generated comments into accurate source locations.
+      fix_po_location,
+      '@INPUT@',
+      '@OUTPUT@',
+    ],
+    # Keep old file name for backwards compatibility.
+    output: 'libmypaint.pot',
+  )
+
+  update_pot = run_target(
+    'libmypaint.pot',
+    command: [
+      cp,
+      pot,
+      meson.current_source_dir(),
+    ],
+    depends: pot,
+  )
+
+  # Update PO files.
+
+  foreach lang : languages
+    pofile = meson.current_source_dir() / f'@lang@.po'
+    update_po_target = f'update-po-@lang@'
+
+    if fs.is_file(pofile)
+      update_po_targets += run_target(
+        update_po_target,
+        command: [
+          msgmerge,
+          '--quiet',
+          '--output-file', pofile,
+          pofile,
+          # `run_target` cannot depend on another `run_target`.
+          # Use .pot file from build directory.
+          pot,
+        ],
+        depends: [
+          pot,
+        ],
+      )
+    else
+      update_po_targets += run_target(
+        update_po_target,
+        command: [
+          msginit,
+          # `run_target` cannot depend on another `run_target`.
+          # Use .pot file from build directory.
+          '--input=' + pot,
+          '--output-file=' + pofile,
+          '--locale=' + lang,
+          '--no-translator',
+        ],
+        depends: [
+          pot,
+        ],
+      )
+    endif
+  endforeach
+
+  alias_target(
+    f'update-po',
+    update_pot,
+    update_po_targets,
+  )
+endif
diff --git a/tests/meson.build b/tests/meson.build
new file mode 100644
index 00000000..c9cba80e
--- /dev/null
+++ b/tests/meson.build
@@ -0,0 +1,92 @@
+tests = [
+  {
+    'name': 'test-brush-load',
+    'timeout': 5,
+  },
+  {
+    'name': 'test-brush-persistence',
+    'timeout': 5,
+  },
+  {
+    'name': 'test-details',
+    'timeout': 60,
+  },
+  {
+    'name': 'test-fixed-tiled-surface',
+    'timeout': 1000,
+  },
+  {
+    'name': 'test-rng',
+    'timeout': 5,
+  },
+]
+
+if use_gegl
+  tests += {
+    'name': 'test-gegl-surface',
+    'srcs': 'gegl/test-gegl-surface.c',
+    'deps': [
+      gegl,
+    ],
+    'incs': [
+      libmypaint_gegl_inc,
+    ],
+    'link': [
+      libmypaint_gegl,
+    ],
+    'timeout': 2000,
+  }
+endif
+
+libmypaint_tests_lib = static_library(
+  'mypaint-tests',
+  'mypaint-benchmark.c',
+  'mypaint-test-surface.c',
+  'mypaint-utils-stroke-player.c',
+  'testutils.c',
+  brush_settings_headers,
+  c_args: [
+    '-DLIBMYPAINT_TESTING_ABS_TOP_SRCDIR="@0@"'.format(meson.project_source_root()),
+  ],
+  include_directories: toplevel_inc,
+  dependencies: [
+    gobject,
+  ],
+)
+
+foreach test : tests
+  test_name = test.get('name')
+  test_srcs = test.get('srcs', test_name + '.c')
+  test_deps = test.get('deps', [])
+  test_incs = test.get('incs', [])
+  test_link = test.get('link', [])
+  test_timeout = test.get('timeout', 30)
+
+  test_exe = executable(
+    test_name,
+    test_srcs,
+    brush_settings_headers,
+    c_args: '-DLIBMYPAINT_TESTING_ABS_TOP_SRCDIR="@0@"'.format(meson.project_source_root()),
+    include_directories: [
+      toplevel_inc,
+      test_incs,
+    ],
+    link_with: [
+      libmypaint,
+      libmypaint_tests_lib,
+      test_link,
+    ],
+    dependencies: [
+      gobject,
+      libmath,
+      libprofiler,
+      test_deps,
+    ],
+  )
+
+  test(
+    test_name,
+    test_exe,
+    timeout: test_timeout,
+  )
+endforeach

From c9243c079887f32d892ba5bb963e091d765a71e9 Mon Sep 17 00:00:00 2001
From: Jan Tojnar <jtojnar@gmail.com>
Date: Mon, 5 Aug 2024 00:23:24 +0200
Subject: [PATCH 3/3] Update developer docs to use Meson

Autotools are now deprecated.

Also remove a maintenance script that is now handled by Meson.
---
 README.md                 |  87 ++++++++++-------------
 po/README.md              |  12 ++--
 po/update_translations.sh | 142 --------------------------------------
 3 files changed, 42 insertions(+), 199 deletions(-)
 delete mode 100755 po/update_translations.sh

diff --git a/README.md b/README.md
index 0143fd07..96f38781 100644
--- a/README.md
+++ b/README.md
@@ -12,17 +12,16 @@ License: ISC, see [COPYING](./COPYING) for details.
 ## Dependencies
 
 * All configurations and builds:
+  - C compiler, linker etc.
+  - [Python](http://python.org/)
+  - [Meson](https://en.wikipedia.org/wiki/Meson_(software)) and [ninja](https://en.wikipedia.org/wiki/Ninja_(build_system))
+    - Alternately, Autotools can be used instead but they are now considered deprecated.
   - [json-c](https://github.com/json-c/json-c/wiki) (>= 0.11)
-  - C compiler, `make` etc.
-* Most configurations (all except `--disable-introspection --without-glib`):
+  - [gettext](https://www.gnu.org/software/gettext/gettext.html) (unless `-Di18n=disabled`)
+* Most configurations (all except `-Dintrospection=disabled -Dglib=disabled`):
   - [GObject-Introspection](https://live.gnome.org/GObjectIntrospection)
   - [GLib](https://wiki.gnome.org/Projects/GLib)
-* When building from `git` (developer package names vary by distribution):
-  - [Python](http://python.org/)
-  - [autotools](https://en.wikipedia.org/wiki/GNU_Build_System)
-  - [intltool](https://freedesktop.org/wiki/Software/intltool/)
-  - [gettext](https://www.gnu.org/software/gettext/gettext.html)
-* For `--enable-gegl` (GIMP *does not* require this):
+* For `-Dgegl=enabled` (GIMP *does not* require this):
   - [GEGL + BABL](http://gegl.org/)
 
 ### Install dependencies (Debian and derivatives)
@@ -30,10 +29,10 @@ License: ISC, see [COPYING](./COPYING) for details.
 On recent Debian-like systems, you can type the following
 to get started with a standard configuration:
 
-    # apt install -y build-essential
+    # apt install -y build-essential meson
     # apt install -y libjson-c-dev libgirepository1.0-dev libglib2.0-dev
 
-When building from git:
+Additionally, when using the deprecated Autotools build system:
 
     # apt install -y python autotools-dev intltool gettext libtool
     
@@ -44,11 +43,11 @@ You might also try using your package manager:
 
 ### Install dependencies (Red Hat and derivatives)
 
-The following works on a minimal CentOS 7 installation:
+The following should works on a minimal CentOS 7 installation:
 
-    # yum install -y gcc gobject-introspection-devel json-c-devel glib2-devel
+    # yum install -y gcc meson gobject-introspection-devel json-c-devel glib2-devel
 
-When building from git, you'll want to add:
+Additionally, when using the deprecated Autotools build system:
 
     # yum install -y git python autoconf intltool gettext libtool
     
@@ -60,9 +59,9 @@ You might also try your package manager:
 
 Works with a fresh OpenSUSE Tumbleweed Docker image:
 
-    # zypper install gcc13 gobject-introspection-devel libjson-c-devel glib2-devel
+    # zypper install gcc13 meson gobject-introspection-devel libjson-c-devel glib2-devel
 
-When building from git:
+Additionally, when using the deprecated Autotools build system:
 
     # zypper install git python311 autoconf intltool gettext-tools libtool
 
@@ -72,61 +71,53 @@ Package manager:
 
 ## Build and install
 
+You can use [Meson](https://mesonbuild.com/) build system.
+
+    $ meson setup _build --prefix=/usr
+    $ meson compile -C _build
+    # meson install -C _build
+    # ldconfig
+
 MyPaint and libmypaint benefit dramatically from autovectorization and other compiler optimizations.
-You may want to set your CFLAGS before compiling (for gcc):
+You may want to set your CFLAGS before compiling (for gcc) by passing something like the following as an argument to `meson setup`:
 
-    $ export CFLAGS='-Ofast -ftree-vectorize -fopt-info-vec-optimized -march=native -mtune=native -funsafe-math-optimizations -funsafe-loop-optimizations'
+    -Dc_args='-Ofast -ftree-vectorize -fopt-info-vec-optimized -march=native -mtune=native -funsafe-math-optimizations -funsafe-loop-optimizations'
 
-The traditional setup works just fine.
+Alternately, you can use the traditional Autotools build system for now (it is deprecated and will be eventually removed):
 
-    $ ./autogen.sh    # Only needed when building from git.
+    $ ./autogen.sh
     $ ./configure
     # make install
     # ldconfig
 
-### Maintainer mode
-
-We don't ship a `configure` script in our git repository. If you're
-building from git, you have to kickstart the build environment with:
-
-    $ git clone https://github.com/mypaint/libmypaint.git
-    $ cd libmypaint
-    $ ./autogen.sh
-
-This script generates `configure` from `configure.ac`, after running a
-few checks to make sure your build environment is broadly OK. It also
-regenerates certain important generated headers if they need it.
-
-Folks building from a release tarball don't need to do this: they will
-have a `configure` script from the start.
-
 ### Configure
 
-    $ ./configure
-    $ ./configure --prefix=/tmp/junk/example
+Meson requires out-of-tree builds so you need to specify a build directory.
 
-There are several MyPaint-specific options.
-These can be shown by running
+    $ meson setup _build
+    $ meson setup _build --prefix=/tmp/junk/example
 
-    $ ./configure --help
+In addition to to [Meson options](https://mesonbuild.com/Builtin-options.html#compiler-options), there are several libmypaint-specific options, see `meson_options.txt` file for details.
+
+    $ meson setup _build -Dgegl=disabled -Ddocs=true -Di18n=enabled
 
 ### Build
 
-    $ make
+    $ meson compile -C _build
 
 Once MyPaint is built, you can run the test suite and/or install it.
 
 ### Test
 
-    $ make check
+    $ meson test -C _build
 
 This runs all the unit tests.
 
 ### Install
 
-    # make install
+    # meson install -C _build
 
-Uninstall libmypaint with `make uninstall`.
+Uninstall libmypaint with `meson uninstall -C _build`.
 
 ### Check availability
 
@@ -172,11 +163,7 @@ for details of how you can begin contributing.
 
 The distribution release can be generated with:
 
-    $ make dist
-
-And it should be checked before public release with:
-
-    $ make distcheck
+    $ meson dist -C _build
 
 ## Localization
 
@@ -198,7 +185,7 @@ in `po/POTFILES.in`.
 You can update the .po files when translated strings in the code change
 using:
 
-    $ cd po && make update-po
+    $ meson compile -C _build update-po
 
 When the results of this are pushed, Weblate translators will see the
 new strings immediately.
diff --git a/po/README.md b/po/README.md
index aca2d611..66366acd 100644
--- a/po/README.md
+++ b/po/README.md
@@ -16,7 +16,7 @@ We use [GNU gettext][gettext] for runtime translation of program text.
 ## After updating program strings
 
 After changing any string in the source text which makes use of the
-gettext macros, you will need to run `./po/update_translation.sh`
+gettext macros, you will need to run `meson compile -C _build update-po`
 and then commit the modified `po/libmypaint.pot` and `po/*.po`
 files along with your changes.
 Keeping this generated template file in the distribution
@@ -26,7 +26,7 @@ without having to ask us.
 if all you want to do is compare diffs,
 the `.pot` file alone can be updated by running:
 ```
-./po/update_translations.sh --only-template
+meson compile -C _build libmypaint.pot
 ```
 
 # Information for translators
@@ -63,18 +63,16 @@ Before working on a translation,
 update the `.po` file for your language.
 For example, for the French translation, run:
 
-    ./po/update_translations.sh fr
+    meson compile -C _build update-po-fr
 
 ## Use/Test the translation
 
 After modifying the translation,
 you need to rebuild and reinstall libmypaint to see the effect
 in MyPaint, or in other apps that use `libmypaint`.
-If you have already built it, and `make install` does what you want,
-you can just use:
+If you have already built it, you can just use:
 
-    make
-    make install
+    meson install -C _build
 
 To run MyPaint with a specific translation on Linux,
 you can use the LANG environment variable
diff --git a/po/update_translations.sh b/po/update_translations.sh
deleted file mode 100755
index 1d0b919b..00000000
--- a/po/update_translations.sh
+++ /dev/null
@@ -1,142 +0,0 @@
-#!/usr/bin/env bash
-
-update_translations()
-{
-    cd $(dirname "$1")
-    local HELP
-    HELP="\
-====
-Update translation files: generated sources / po template / po language files
-Usage: $1 [--force] [[--only-template] | [LANG...]]
-
-If no languages are specified, and --only-template is not set, all .po files in
-the directory will be updated, same as running: $1 *.po
-====
-"
-    shift
-
-    sec() { date -r "$1" "+%s"; }
-    err() { >&2 echo -e "\e[91m""Error: $@""\e[0m"; }
-    print_errors()
-    {
-        for e in "$@"
-        do
-            err "$e"
-        done
-    }
-
-    local ORIG GEN_SRC ENUM GEN TEMPLATE FORCE ONLY_TEMPLATE
-    ORIG=../brushsettings.json
-    GEN_SRC=../brushsettings-gen.h
-    ENUM=../mypaint-brush-settings-gen.h
-    GEN_SCRIPT=../generate.py
-    TEMPLATE=libmypaint.pot
-
-    local langs errors
-    langs=()
-    errors=()
-
-    while [ -n "$1" ]
-    do
-        case "$1" in
-            --help)
-                echo "$HELP" && exit 0
-                ;;
-            --force)
-                FORCE=1
-                ;;
-            --only-template)
-                ONLY_TEMPLATE=1
-                ;;
-            -*)
-                errors+=("Unrecognized option: $1")
-                ;;
-            *)
-                local f
-                f="${1%%.po}.po"
-                if [ ! -e "$f" ]
-                then
-                    errors+=("Not found: $f - LANG must be the code or .po file for an existing language")
-                else
-                    langs+=("$f")
-                fi
-                ;;
-        esac
-        shift
-    done
-
-    # Sanity check
-    if [ -n "$ONLY_TEMPLATE" -a -n "$langs" ]
-    then
-        errors+=("Don't specify languages when using ``--only-template``")
-    fi
-    # Print usage instructions followed by error message(s)
-    [ -n "$errors" ] && >&2 echo "$HELP" && print_errors "${errors[@]}" && exit 1
-
-    # Check if the message source file needs to be (re)generated.
-    # ( generated source: not present,older than basis, older than script )
-    if [ -n "$FORCE" -o ! -e "$GEN_SRC" -o \
-            $(sec "$GEN_SRC") -lt $(sec "$ORIG") -o  \
-            $(sec "$GEN_SRC") -lt $(sec "$GEN_SCRIPT") ]
-    then
-        [ -z "$FORCE" ] &&
-            echo "Generated file missing or out of date, generating..." ||
-                echo "Generating (forced)..."
-        python "$GEN_SCRIPT" "$ENUM" "$GEN_SRC" ||
-            (echo "Failed to generate source file!" && exit 1)
-    fi
-
-    # Check if the template file appears up to date
-    if [ -z "$FORCE" -a -e "$TEMPLATE" -a $(sec "$GEN_SRC") -lt $(sec "$TEMPLATE") ]
-    then
-        echo "$TEMPLATE up to date, skipping extraction (use --force to override)."
-    else
-        local temp_template temp_diff
-        temp_template=$(mktemp)
-        temp_diff=$(mktemp)
-        # Omit locations from the generated file, and instead...
-        xgettext --no-location -c -kN_:1 -o - "$GEN_SRC" |
-            # ...transform special generated comments into accurate source locations.
-            sed -E "s@^#\. (: $ORIG:.*)@#\1@" > "$temp_template"
-        # Don't update template if the only change is the creation date
-        diff --suppress-common-lines -y "$TEMPLATE" "$temp_template" > "$temp_diff"
-        if [ $(wc -l < "$temp_diff") -eq 1 -a
-             $(grep -i -o "POT-Creation-Date" "$temp_diff" | wc -l) -eq 2 ]
-        then
-            echo "$TEMPLATE unchanged"
-        else
-            mv "$temp_template" "$TEMPLATE" && echo "$TEMPLATE updated."
-        fi
-    fi
-
-    # If requested, don't update any languages
-    [ -n "$ONLY_TEMPLATE" ] && exit 0
-
-    # If no languages are specified, try to update all of them
-    if [ -z "${langs[*]}" ]
-    then
-        langs=($(ls *.po))
-    fi
-
-    local failed_updates
-    failed_updates=()
-
-    # Update the language files based on the template
-    for lang in "${langs[@]}"
-    do
-        msgmerge -q -U "$lang" "$TEMPLATE" || failed_updates+=("$lang")
-    done
-
-    echo "Successfully processed $((${#langs[@]} - ${#failed_updates[@]})) language files."
-    if [ -n "${failed_updates[*]}" ]
-    then
-        err "Failed to update ${#failed_updates[@]} language files:"
-        for f in "${failed_updates[@]}"
-        do
-            err "$f"
-        done
-        exit 1
-    fi
-}
-
-update_translations "$0" "$@"