diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 00000000..80b3b48a --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,37 @@ +name: Test + +on: + - push + - pull_request + +jobs: + linux: + name: Linux + runs-on: ubuntu-22.04 + strategy: + matrix: + configureFlags: + - "" + - "--with-introspection" + - "--with-gegl" + include: + - configureFlags: "--with-introspection" + extraDeps: "libgirepository1.0-dev" + - configureFlags: "--with-gegl" + extraDeps: "libgegl-dev" + steps: + - uses: actions/checkout@v4 + - name: "Install dependencies" + run: | + sudo apt-get update + sudo apt-get install -y \ + libjson-c-dev \ + intltool \ + ${{ matrix.extraDeps }} + - name: "Build" + run: | + ./autogen.sh + ./configure ${{ matrix.configureFlags }} + make + - name: "Run tests" + run: make distcheck diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 1c84b87a..00000000 --- a/.travis.yml +++ /dev/null @@ -1,32 +0,0 @@ -language: c - -compiler: - - gcc - -sudo: required - -dist: trusty - -addons: - apt: - packages: - - libjson0-dev - - intltool - - libgirepository1.0-dev - - libgegl-dev - -script: - - ./autogen.sh - - ./configure - - make - - make clean - - make distcheck - - make maintainer-clean - - ./autogen.sh - - ./configure --with-introspection - - make - - make maintainer-clean - - ./autogen.sh - - ./configure --with-gegl - - make - - make maintainer-clean diff --git a/README.md b/README.md index fae90566..d278bac5 100644 --- a/README.md +++ b/README.md @@ -30,31 +30,45 @@ License: ISC, see [COPYING](./COPYING) for details. On recent Debian-like systems, you can type the following to get started with a standard configuration: - $ sudo apt install -y build-essential - $ sudo apt install -y libjson-c-dev libgirepository1.0-dev libglib2.0-dev + # apt install -y build-essential + # apt install -y libjson-c-dev libgirepository1.0-dev libglib2.0-dev When building from git: - $ sudo apt install -y python autotools-dev intltool gettext libtool + # apt install -y python autotools-dev intltool gettext libtool You might also try using your package manager: - $ sudo apt build-dep mypaint # will get additional deps for MyPaint (GUI) - $ sudo apt build-dep libmypaint # may not exist; included in mypaint + # apt build-dep mypaint # will get additional deps for MyPaint (GUI) + # apt build-dep libmypaint # may not exist; included in mypaint ### Install dependencies (Red Hat and derivatives) The following works on a minimal CentOS 7 installation: - $ sudo yum install -y gcc gobject-introspection-devel json-c-devel glib2-devel + # yum install -y gcc gobject-introspection-devel json-c-devel glib2-devel When building from git, you'll want to add: - $ sudo yum install -y git python autoconf intltool gettext libtool + # yum install -y git python autoconf intltool gettext libtool You might also try your package manager: - - $ sudo yum builddep libmypaint + + # yum builddep libmypaint + +### Install dependencies (OpenSUSE) + +Works with a fresh OpenSUSE Tumbleweed Docker image: + + # zypper install gcc13 gobject-introspection-devel libjson-c-devel glib2-devel + +When building from git: + + # zypper install git python311 autoconf intltool gettext-tools libtool + +Package manager: + + # zypper install libmypaint0 ## Build and install @@ -67,8 +81,8 @@ The traditional setup works just fine. $ ./autogen.sh # Only needed when building from git. $ ./configure - $ sudo make install - $ sudo ldconfig + # make install + # ldconfig ### Maintainer mode @@ -110,7 +124,7 @@ This runs all the unit tests. ### Install - $ sudo make install + # make install Uninstall libmypaint with `make uninstall`. @@ -123,22 +137,22 @@ Make sure that pkg-config can see libmypaint before trying to build with it. If it's not found, you'll need to add the relevant pkgconfig directory to the `pkg-config` search path. For example, on CentOS, with a default install: - $ sudo sh -c "echo 'PKG_CONFIG_PATH=/usr/local/lib/pkgconfig' >>/etc/environment" + # sh -c "echo 'PKG_CONFIG_PATH=/usr/local/lib/pkgconfig' >>/etc/environment" Make sure ldconfig can see libmypaint as well - $ sudo ldconfig -p |grep -i libmypaint + # ldconfig -p |grep -i libmypaint If it's not found, you'll need to add the relevant lib directory to the LD_LIBRARY_PATH: $ export LD_LIBRARY_PATH=/usr/local/lib - $ sudo sh -c "echo 'LD_LIBRARY_PATH=/usr/local/lib' >>/etc/environment + # sh -c "echo 'LD_LIBRARY_PATH=/usr/local/lib' >>/etc/environment Alternatively, you may want to enable /usr/local for libraries. Arch and Redhat derivatives: - $ sudo sh -c "echo '/usr/local/lib' > /etc/ld.so.conf.d/usrlocal.conf" - $ sudo ldconfig + # sh -c "echo '/usr/local/lib' > /etc/ld.so.conf.d/usrlocal.conf" + # ldconfig ## Contributing diff --git a/autogen.sh b/autogen.sh index 625f137c..5adf32e6 100755 --- a/autogen.sh +++ b/autogen.sh @@ -9,10 +9,10 @@ # tools and you shouldn't use this script. Just call ./configure # directly. -ACLOCAL=${ACLOCAL-aclocal-1.16} +ACLOCAL=${ACLOCAL-aclocal-1.17} AUTOCONF=${AUTOCONF-autoconf} AUTOHEADER=${AUTOHEADER-autoheader} -AUTOMAKE=${AUTOMAKE-automake-1.16} +AUTOMAKE=${AUTOMAKE-automake-1.17} LIBTOOLIZE=${LIBTOOLIZE-libtoolize} PYTHON=${PYTHON-python} @@ -128,6 +128,9 @@ printf "checking for automake >= $AUTOMAKE_REQUIRED_VERSION ... " if ($AUTOMAKE --version) < /dev/null > /dev/null 2>&1; then AUTOMAKE=$AUTOMAKE ACLOCAL=$ACLOCAL +elif (automake-1.17 --version) < /dev/null > /dev/null 2>&1; then + AUTOMAKE=automake-1.17 + ACLOCAL=aclocal-1.17 elif (automake-1.16 --version) < /dev/null > /dev/null 2>&1; then AUTOMAKE=automake-1.16 ACLOCAL=aclocal-1.16 diff --git a/generate.py b/generate.py index 231f0c6e..e4018775 100644 --- a/generate.py +++ b/generate.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # libmypaint - The MyPaint Brush Library # Copyright (C) 2007-2012 Martin Renold -# Copyright (C) 2012-2016 by the MyPaint Development Team. +# Copyright (C) 2012-2020 by the MyPaint Development Team. # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above @@ -20,14 +20,56 @@ from __future__ import absolute_import, division, print_function +import json +import json.decoder +import json.scanner import os import sys from os.path import basename -import json from collections import namedtuple + PY3 = sys.version_info >= (3,) +# == JSON parser wrapper == # + +# In order to get the line number from the original json file, this minimal +# wrapper of the standard parser produces tuples for string values where +# the first item is a (line, column) tuple. Complexity is quadratic due to +# naive (but convenient) calculations of lines/columns, so don't use this +# hack for anything heavy. + + +def _linecol(s, pos): + ss = s[:pos] + line = ss.count('\n') + 1 + column = len(ss[ss.rfind('\n') + 1:]) + 1 + return (line, column) + + +def _linecol_scanstring(s, end, *args, **kwds): + result, real_end = json.decoder.py_scanstring(s, end, *args, **kwds) + return (_linecol(s, end - 1), result), real_end + + +def _normalize(n): # Strip away the linecol component from keys + def _n(t): + return t[1] if isinstance(t, tuple) else t + if isinstance(n, dict): + return {_n(k): _normalize(v) for k, v in n.items()} + elif isinstance(n, list): + return [_normalize(v) for v in n] + else: + return n + + +def loads(*args, **kwds): + decoder = json.decoder.JSONDecoder() + decoder.parse_string = _linecol_scanstring + decoder.scan_once = json.scanner.py_make_scanner(decoder) + return _normalize(decoder.decode(*args, **kwds)) + + # A basic translator comment is generated for each string, # noting whether it is an input or a setting, and for tooltips # stating which input/setting it belongs to. @@ -63,11 +105,18 @@ ] _STATES = [] # brushsettings.states +_ORIG_FILE = "brushsettings.json" + class _BrushSetting (namedtuple("_BrushSetting", _SETTING_ORDER)): + def __init__(self, *args, **kwds): + super(_BrushSetting, self).__init__() + self.real_internal_name = self.internal_name[1] + self.real_displayed_name = self.displayed_name[1] + def validate(self): - msg = "Failed to validate %s: %r" % (self.internal_name, self) + msg = "Failed to validate %s: %r" % (self.real_internal_name, self) if self.minimum and self.maximum: assert (self.minimum <= self.default), msg assert (self.maximum >= self.default), msg @@ -77,8 +126,14 @@ def validate(self): class _BrushInput (namedtuple("_BrushInput", _INPUT_ORDER)): + def __init__(self, *args, **kwds): + self.anything = "nothing" + super(_BrushInput, self).__init__() + self.real_id = self.id[1] + self.real_displayed_name = self.displayed_name[1] + def validate(self): - msg = "Failed to validate %s: %r" % (self.id, self) + msg = "Failed to validate %s: %r" % (self.real_id, self) if self.hard_maximum is not None: assert (self.hard_maximum >= self.soft_maximum), msg assert (self.hard_maximum >= self.normal), msg @@ -103,7 +158,7 @@ def with_comments(d): flag = "r" if PY3 else "rb" with open(filename, flag) as fp: - defs = json.load(fp) + defs = loads(fp.read()) for input_def in defs["inputs"]: input = _BrushInput(**with_comments(input_def)) input.validate() @@ -112,7 +167,7 @@ def with_comments(d): setting = _BrushSetting(**with_comments(setting_def)) setting.validate() _SETTINGS.append(setting) - for state_name in defs["states"]: + for _, state_name in defs["states"]: _STATES.append(state_name) @@ -167,8 +222,10 @@ def floatify(value, positive_inf=True): return str(value) -def gettextify(value, comment=None): +def gettextify(annotated_value, comment=None): + (line, _), value = annotated_value result = "N_(%s)" % stringify(value) + result = "/*: ../%s:%d */ %s" % (_ORIG_FILE, line, result) if comment: assert isinstance(comment, str) or isinstance(comment, unicode) result = "/* %s */ %s" % (comment, result) @@ -182,7 +239,7 @@ def boolify(value): def tcomment(base_comment, addendum=None): comment = base_comment if addendum: - comment = "{c} - {a}".format(c=comment, a=addendum) + comment = "{c} - {a}".format(c=comment, a=addendum[1]) return comment @@ -195,9 +252,9 @@ def tooltip_comment(name, name_type, addendum=None): def input_info_struct(i): name_comment = tcomment("Brush input", i.tcomment_name) _tooltip_comment = tooltip_comment( - i.displayed_name, "input", i.tcomment_tooltip) + i.real_displayed_name, "input", i.tcomment_tooltip) return ( - stringify(i.id), + stringify(i.real_id), floatify(i.hard_minimum, positive_inf=False), floatify(i.soft_minimum, positive_inf=False), floatify(i.normal), @@ -211,9 +268,9 @@ def input_info_struct(i): def settings_info_struct(s): name_comment = tcomment("Brush setting", s.tcomment_name) _tooltip_comment = tooltip_comment( - s.displayed_name, "setting", s.tcomment_tooltip) + s.real_displayed_name, "setting", s.tcomment_tooltip) return ( - stringify(s.internal_name), + stringify(s.real_internal_name), gettextify(s.displayed_name, name_comment), boolify(s.constant), floatify(s.minimum, positive_inf=False), @@ -256,14 +313,14 @@ def generate_public_settings_code(): "MyPaintBrushInput", "MYPAINT_BRUSH_INPUT_", "MYPAINT_BRUSH_INPUTS_COUNT", - enumerate([i.id.upper() for i in _INPUTS]), + enumerate([i.real_id.upper() for i in _INPUTS]), ) content += '\n' content += generate_enum( "MyPaintBrushSetting", "MYPAINT_BRUSH_SETTING_", "MYPAINT_BRUSH_SETTINGS_COUNT", - enumerate([i.internal_name.upper() for i in _SETTINGS]), + enumerate([i.real_internal_name.upper() for i in _SETTINGS]), ) content += '\n' content += generate_enum( @@ -277,8 +334,9 @@ def generate_public_settings_code(): if __name__ == '__main__': - _init_globals_from_json("brushsettings.json") script = sys.argv[0] + source = os.path.join(os.path.dirname(script), _ORIG_FILE) + _init_globals_from_json(source) try: public_header_file, internal_header_file = sys.argv[1:] except Exception: diff --git a/mypaint-brush.c b/mypaint-brush.c index ba1e921a..52005b87 100644 --- a/mypaint-brush.c +++ b/mypaint-brush.c @@ -158,7 +158,7 @@ brush_reset(MyPaintBrush *self) int min_index = self->min_bucket_used; if (min_index != -1) { int max_index = self->max_bucket_used; - size_t num_bytes = (max_index - min_index) * sizeof(self->smudge_buckets[0]) * SMUDGE_BUCKET_SIZE; + size_t num_bytes = (max_index - min_index + 1) * sizeof(self->smudge_buckets[0]) * SMUDGE_BUCKET_SIZE; memset(self->smudge_buckets + min_index, 0, num_bytes); self->min_bucket_used = -1; self->max_bucket_used = -1; diff --git a/mypaint-fixed-tiled-surface.c b/mypaint-fixed-tiled-surface.c index a85051eb..287e324d 100644 --- a/mypaint-fixed-tiled-surface.c +++ b/mypaint-fixed-tiled-surface.c @@ -68,7 +68,7 @@ tile_request_end(MyPaintTiledSurface *tiled_surface, MyPaintTileRequest *request const int ty = request->ty; if (tx >= self->tiles_width || ty >= self->tiles_height || tx < 0 || ty < 0) { - // Wipe any changed done to the null tile + // Wipe any changes done to the null tile reset_null_tile(self); } else { // We hand out direct pointers to our buffer, so for the normal case nothing needs to be done diff --git a/po/README.md b/po/README.md index e9c93429..aca2d611 100644 --- a/po/README.md +++ b/po/README.md @@ -16,24 +16,18 @@ 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 manually run - - cd po/ - intltool-update -g libmypaint --pot - - for lang in `cat LINGUAS`; do - intltool-update -g libmypaint --dist $lang - done - +gettext macros, you will need to run `./po/update_translation.sh` and then commit the modified `po/libmypaint.pot` and `po/*.po` files along with your changes. Keeping this generated template file in the distribution allows WebLate users to create new translations by themselves without having to ask us. -The `.pot` file alone can be updated by running -just the first `intltool-update` command, -if all you want to do is compare diffs. +if all you want to do is compare diffs, +the `.pot` file alone can be updated by running: +``` +./po/update_translations.sh --only-template +``` # Information for translators @@ -69,7 +63,7 @@ Before working on a translation, update the `.po` file for your language. For example, for the French translation, run: - intltool-update -g libmypaint --dist fr + ./po/update_translations.sh fr ## Use/Test the translation diff --git a/po/update_translations.sh b/po/update_translations.sh new file mode 100755 index 00000000..1d0b919b --- /dev/null +++ b/po/update_translations.sh @@ -0,0 +1,142 @@ +#!/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" "$@"