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/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/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 ", 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/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/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/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. 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/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/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/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" "$@" 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