Philipp Hörist pushed to branch port-pyproject at gajim / gajim
Commits: b93a1ad9 by Philipp Hörist at 2022-12-31T00:32:36+01:00 new: Add custom PEP517 build backend
- - - - - 784641b8 by Philipp Hörist at 2022-12-31T00:32:36+01:00 refactor: Load translations from package data directory
- - - - - 1d679212 by Philipp Hörist at 2022-12-31T00:32:36+01:00 ci: Use `build` to build Gajim
- - - - -
9 changed files:
- .gitlab-ci.yml - MANIFEST.in - README.md - gajim/common/i18n.py - + pep517build/__init__.py - + pep517build/backend.py - + pep517build/install_metadata.py - pyproject.toml - − setup.py
Changes:
===================================== .gitlab-ci.yml ===================================== @@ -15,8 +15,9 @@ test-other: stage: test script: - codespell gajim - - python3 setup.py build - - appstream-util validate build/data/org.gajim.Gajim.appdata.xml + - pip3 install build + - python3 -m build -w + - appstream-util validate dist/share/metainfo/org.gajim.Gajim.appdata.xml interruptible: true
test-pyright: @@ -68,7 +69,8 @@ build-linux: script: - apt-get update - apt-get install -y python3-nbxmpp-nightly - - python3 setup.py sdist + - pip3 install build + - python3 -m build -s - python3 .ci/debian_build.py "$(find dist/gajim-*)" 1
artifacts:
===================================== MANIFEST.in ===================================== @@ -1,4 +1,6 @@ include COPYING +include ChangeLog +recursive-include pep517build * recursive-include po * recursive-include data *.1 *.in -recursive-include test * +recursive-include test * \ No newline at end of file
===================================== README.md ===================================== @@ -39,11 +39,49 @@ #### For Video and Audio Calls - [gst-plugins-ugly](https://gitlab.freedesktop.org/gstreamer/gst-plugins-ugly) - [gst-libav](https://gitlab.freedesktop.org/gstreamer/gst-libav)
-### Install Requirements +### Build Requirements
- [setuptools](https://pypi.org/project/setuptools/) (>=65.0.0) - [gettext](https://savannah.gnu.org/projects/gettext/)
+To build Gajim a PEP517 build frontend like pip (https://pip.pypa.io/en/stable/) or build (https://pypa-build.readthedocs.io/en/stable/) must be used. + +The build frontend takes care of installing all python build requirements. Beware `gettext` is not a python library and cannot be installed by the build frontend. + +Execute the following inside the source directory + +#### Building the wheel and metadata files + +A wheel and additional metadata files are placed in the `dist` folder. +The metadata files need to be manually installed afterwards depending on the platform. + +##### Using `build` + +```bash +python -m build -w +``` + +##### Using `pip` + +```bash +pip wheel --no-deps --use-pep517 -w dist . +``` + +#### Installing + +```bash +pip install dist/name_of_wheel.whl +``` + +The wheel building step can be skipped with `pip` + +```bash +pip install . +``` + +in this case the metadata files are still placed into the `dist` directory and have +to be installed manually. + ### Running Tests
- `python -m unittest discover -s test` (no_gui directory) @@ -68,10 +106,6 @@ #### Snapshots - [Daily Linux](https://www.gajim.org/downloads/snap/) - [Daily Windows](https://gajim.org/downloads/snap/win)
-#### Linux - - pip install . - #### Mac
see [Wiki](https://dev.gajim.org/gajim/gajim/-/wikis/help/Gajim-on-macOS)
===================================== gajim/common/i18n.py ===================================== @@ -20,56 +20,19 @@
from __future__ import annotations
-from typing import Iterator +from typing import cast from typing import Optional
import os import sys import locale import gettext +import importlib.resources import unicodedata from pathlib import Path -from gi.repository import GLib - -DOMAIN = 'gajim' -_translation = gettext.NullTranslations() - - -def get_locale_dirs() -> Optional[list[Path]]: - if sys.platform == 'win32': - return - - path = gettext.find(DOMAIN) - if path is not None: - # gettext can find the location itself - # so we don’t need the localedir - return - - if Path('/app/share/run-as-flatpak').exists(): - # Check if we run as flatpak - return [Path('/app/share/')] - - data_dirs = [GLib.get_user_data_dir()] + GLib.get_system_data_dirs() - return [Path(dir_) for dir_ in data_dirs]
-def iter_locale_dirs() -> Iterator[Optional[Path]]: - locale_dirs = get_locale_dirs() - if locale_dirs is None: - yield None - return - - # gettext fallback - locale_dirs.append(Path(sys.base_prefix) / 'share') - - found_paths: list[Path] = [] - for path in locale_dirs: - locale_dir = path / 'locale' - if locale_dir in found_paths: - continue - found_paths.append(locale_dir) - if locale_dir.is_dir(): - yield locale_dir +DOMAIN = 'gajim'
def get_win32_default_lang() -> str: @@ -160,19 +123,16 @@ def ngettext(s_sing: str, # doesn't retain other region settings like LC_TIME os.environ['LANGUAGE'] = LANG
-# Search for the translation in all locale dirs -for dir_ in iter_locale_dirs(): - try: - _translation = gettext.translation(DOMAIN, dir_) - _ = _translation.gettext - if hasattr(locale, 'bindtextdomain'): - locale.bindtextdomain(DOMAIN, dir_) - except OSError: - continue - else: - break - -else: + +package_dir = cast(Path, importlib.resources.files('gajim')) +locale_dir = package_dir / 'data' / 'locale' + +try: + _translation = gettext.translation(DOMAIN, locale_dir) + _ = _translation.gettext + if hasattr(locale, 'bindtextdomain'): + locale.bindtextdomain(DOMAIN, locale_dir) +except OSError: + _translation = gettext.NullTranslations() _ = _translation.gettext print('No translations found for', LANG, file=sys.stderr) - print('Dirs searched: %s' % get_locale_dirs(), file=sys.stderr)
===================================== pep517build/__init__.py =====================================
===================================== pep517build/backend.py ===================================== @@ -0,0 +1,150 @@ +from __future__ import annotations + +from typing import Any + +import gzip +import logging +import shutil +import subprocess +import sys +from pathlib import Path + +import setuptools.build_meta as _orig + + +logging.basicConfig(level='INFO', format='%(levelname)s: %(message)s') +log = logging.getLogger() + +DEFAULT_METADATA_PATH = Path('dist/metadata') +ALLOWED_CONFIG_SETTINGS = {'target'} +MAN_FILES = [ + Path('data/gajim.1'), + Path('data/gajim-remote.1') +] +META_FILES = [ + (Path('data/org.gajim.Gajim.desktop.in'), '--desktop'), + (Path('data/org.gajim.Gajim.appdata.xml.in'), '--xml') +] +ICONS = [ + Path('gajim/data/icons/hicolor/scalable/apps/org.gajim.Gajim.svg'), + Path('gajim/data/icons/hicolor/scalable/apps/org.gajim.Gajim-symbolic.svg'), +] + + +def build_translations() -> None: + # Compile translation files and place them into "gajim/data/locale" + + source_dir = Path.cwd() + translation_dir = source_dir / 'po' + locale_dir = source_dir / 'gajim' / 'data' / 'locale' + + langs = sorted([lang.stem for lang in translation_dir.glob('*.po')]) + + for lang in langs: + po_file = source_dir / 'po' / f'{lang}.po' + mo_file = locale_dir / lang / 'LC_MESSAGES' / 'gajim.mo' + mo_file.parent.mkdir(parents=True, exist_ok=True) + + log.info('Compile %s >> %s', po_file, mo_file) + + subprocess.run(['msgfmt', + str(po_file), + '-o', + str(mo_file)], + check=True) + + +def build_man(target_path: Path) -> None: + # Build man files in target path + + for man_path in MAN_FILES: + data = man_path.read_bytes() + man_file_name = man_path.name + + man_out_path = target_path / f'{man_file_name}.gz' + log.info('Compress %s >> %s', man_file_name, man_out_path) + + with gzip.open(man_out_path, 'wb') as f_out: + f_out.write(data) + + + +def build_intl(target_path: Path) -> None: + # Merge translation files into desktop and metadata files + + for file_path, option in META_FILES: + out_path = target_path / file_path.name + out_path = out_path.with_suffix('') + + log.info('Compile %s >> %s', file_path, out_path) + + subprocess.run(['msgfmt', + option, + '-d', + 'po', + '--template', + str(file_path), + '-o', + str(out_path)], + check=True) + + +def build_app_icons(target_path: Path) -> None: + for file_path in ICONS: + out_path = target_path / file_path.name + + log.info('Copy %s >> %s', file_path, out_path) + shutil.copy2(file_path, out_path) + + +def _check_config_settings(config_settings: dict[str, str]) -> None: + settings = set(config_settings.keys()) - ALLOWED_CONFIG_SETTINGS + if settings: + raise ValueError('Unknown config setting %s' % settings) + + +def _get_target_path(config_settings: dict[str, str] | None) -> Path: + if config_settings is None: + path = DEFAULT_METADATA_PATH + else: + path = Path(config_settings.get('target', DEFAULT_METADATA_PATH)) + + path.mkdir(parents=True, exist_ok=True) + return path + + +def get_requires_for_build_sdist(*args: Any, **kwargs: Any) -> str: + return _orig.get_requires_for_build_sdist(*args, **kwargs) + +def build_sdist(*args: Any, **kwargs: Any) -> str: + return _orig.build_sdist(*args, **kwargs) + +def get_requires_for_build_wheel(*args: Any, **kwargs: Any) -> str: + return _orig.get_requires_for_build_wheel(*args, **kwargs) + +def prepare_metadata_for_build_wheel(*args: Any, **kwargs: Any) -> str: + return _orig.prepare_metadata_for_build_wheel(*args, **kwargs) + +def build_wheel(wheel_directory: str, + config_settings: dict[str, str] | None = None, + metadata_directory: str | None = None + ) -> str: + + if config_settings is not None: + _check_config_settings(config_settings) + target_path = _get_target_path(config_settings) + + build_translations() + + basename = _orig.build_wheel( + wheel_directory, + config_settings=config_settings, + metadata_directory=metadata_directory, + ) + + if sys.platform not in ('win32', 'darwin'): + build_man(target_path) + build_intl(target_path) + build_app_icons(target_path) + + return basename
===================================== pep517build/install_metadata.py ===================================== @@ -0,0 +1,36 @@ +#!/usr/bin/env python + +from __future__ import annotations + +import argparse +import logging +import shutil +from pathlib import Path + +logging.basicConfig(level='INFO', format='%(levelname)s: %(message)s') +log = logging.getLogger() + +DEFAULT_METADATA_PATH = Path('dist/metadata') + +FILES = { + 'gajim-remote.1.gz': 'share/man/man1', + 'gajim.1.gz': 'share/man/man1', + 'org.gajim.Gajim.desktop': 'share/applications', + 'org.gajim.Gajim-symbolic.svg': 'share/icons/hicolor/scalable/apps', + 'org.gajim.Gajim.svg': 'share/icons/hicolor/scalable/apps', + 'org.gajim.Gajim.appdata.xml': 'share/metainfo', +} + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Install metadata files') + parser.add_argument('prefix', type=Path) + parser.add_argument('--path', type=Path, default=DEFAULT_METADATA_PATH) + args = parser.parse_args() + + for file, path in FILES.items(): + src = args.path / file + dest_dir = args.prefix / path + log.info('Copy %s to %s', src, dest_dir) + if not dest_dir.exists(): + dest_dir.mkdir(parents=True) + shutil.copy(src, dest_dir / file)
===================================== pyproject.toml ===================================== @@ -2,7 +2,8 @@ requires = [ "setuptools >= 65.0.0", ] -build-backend = "setuptools.build_meta" +build-backend = "backend" +backend-path = ["pep517build"]
[project] name = "gajim" @@ -56,19 +57,12 @@ gajim-remote = "gajim.gajim_remote:main" [project.gui-scripts] gajim = "gajim.gajim:main"
-[tool.setuptools] -include-package-data = true - [tool.setuptools.packages.find] include = ["gajim*"] -# Necessary so gajim.data is recogniced as namespace package and all files -# under it are included as package-data -namespaces = true
[tool.setuptools.package-data] -"gajim.data" = [ - "icons/**/index.theme", -] +gajim = ["py.typed"] +"gajim.data" = ["**/*"]
[tool.setuptools.dynamic] version = {attr = "gajim.__version__"} @@ -112,6 +106,7 @@ include = [ "mac/*", "scripts/*", "win/*", + "pep517build/*", "gajim/common/application.py", "gajim/common/call_manager.py", "gajim/common/cert_store.py",
===================================== setup.py deleted ===================================== @@ -1,194 +0,0 @@ -#!/usr/bin/env python3 - -from __future__ import annotations - -from typing import cast - -import logging -import os -import sys - -if sys.version_info < (3, 10): - sys.exit('Gajim needs Python 3.10+') - -import subprocess -from pathlib import Path - -from setuptools import setup -from setuptools.command.build_py import build_py as _build -from setuptools.command.install import install as _install - - -DataFilesT = list[tuple[str, list[str]]] - - -MAN_FILES = [ - 'gajim.1', - 'gajim-remote.1' -] -META_FILES = [ - ('data/org.gajim.Gajim.desktop', 'share/applications', '--desktop'), - ('data/org.gajim.Gajim.appdata.xml', 'share/metainfo', '--xml')] - - -TRANS_DIR = Path('po') -TRANS_TEMPLATE = TRANS_DIR / 'gajim.pot' -REPO_DIR = Path(__file__).resolve().parent -BUILD_DIR = REPO_DIR / 'build' - -ALL_LINGUAS = sorted([lang.stem for lang in TRANS_DIR.glob('*.po')]) - - -logging.basicConfig(level='INFO', format='%(levelname)s: %(message)s') -log = logging.getLogger() - - -def newer(source: Path, target: Path) -> bool: - if not source.exists(): - raise ValueError('file "%s" does not exist' % source.resolve()) - if not target.exists(): - return True - - from stat import ST_MTIME - mtime1 = source.stat()[ST_MTIME] - mtime2 = target.stat()[ST_MTIME] - - return mtime1 > mtime2 - - -def build_translation() -> None: - for lang in ALL_LINGUAS: - po_file = TRANS_DIR / f'{lang}.po' - mo_file = BUILD_DIR / 'mo' / lang / 'LC_MESSAGES' / 'gajim.mo' - mo_dir = mo_file.parent - if not (mo_dir.is_dir() or mo_dir.is_symlink()): - mo_dir.mkdir(parents=True) - - if newer(po_file, mo_file): - subprocess.run(['msgfmt', - str(po_file), - '-o', - str(mo_file)], - cwd=REPO_DIR, - check=True) - - log.info('Compiling %s >> %s', po_file, mo_file) - - -def install_trans(data_files: DataFilesT) -> None: - for lang in ALL_LINGUAS: - mo_file = str(BUILD_DIR / 'mo' / lang / 'LC_MESSAGES' / 'gajim.mo') - target = f'share/locale/{lang}/LC_MESSAGES' - data_files.append((target, [mo_file])) - - -def build_man() -> None: - ''' - Compress Gajim manual files - ''' - newdir = BUILD_DIR / 'man' - if not (newdir.is_dir() or newdir.is_symlink()): - newdir.mkdir() - - for man in MAN_FILES: - filename = Path('data') / man - man_file_gz = newdir / (man + '.gz') - if man_file_gz.exists(): - if newer(filename, man_file_gz): - man_file_gz.unlink() - else: - continue - - import gzip - # Binary io, so open is OK - with open(filename, 'rb') as f_in,\ - gzip.open(man_file_gz, 'wb') as f_out: - f_out.writelines(f_in) - log.info('Compiling %s >> %s', filename, man_file_gz) - - -def install_man(data_files: DataFilesT) -> None: - man_dir = BUILD_DIR / 'man' - target = 'share/man/man1' - - for man in MAN_FILES: - man_file_gz = str(man_dir / (man + '.gz')) - data_files.append((target, [man_file_gz])) - - -def build_intl() -> None: - ''' - Merge translation files into desktop and mime files - ''' - base = BUILD_DIR - - for filename, _, option in META_FILES: - newfile = base / filename - newdir = newfile.parent - if not (newdir.is_dir() or newdir.is_symlink()): - newdir.mkdir() - merge(Path(filename + '.in'), newfile, option) - - -def install_intl(data_files: DataFilesT) -> None: - for filename, target, _ in META_FILES: - data_files.append((target, [str(BUILD_DIR / filename)])) - - -def merge(in_file: Path, - out_file: Path, - option: str, - po_dir: str = 'po') -> None: - ''' - Run the msgfmt command. - ''' - if in_file.exists(): - cmd = (('msgfmt %(opt)s -d %(po_dir)s --template %(in_file)s ' - '-o %(out_file)s') % - {'opt': option, - 'po_dir': po_dir, - 'in_file': in_file, - 'out_file': out_file}) - if os.system(cmd) != 0: - msg = ('ERROR: %s was not merged into the translation files!\n' % - out_file) - raise SystemExit(msg) - log.info('Compiling %s >> %s', in_file, out_file) - - -class Build(_build): - def run(self): - build_translation() - if sys.platform != 'win32': - build_man() - build_intl() - _build.run(self) - - -class Install(_install): - def run(self): - data_files = cast(DataFilesT, self.distribution.data_files) # pyright: ignore # noqa: E501 - install_trans(data_files) - if sys.platform != 'win32': - install_man(data_files) - install_intl(data_files) - _install.run(self) # pyright: ignore - - -# only install subdirectories of data -data_files_app_icon = [ - ('share/icons/hicolor/scalable/apps', - ['gajim/data/icons/hicolor/scalable/apps/org.gajim.Gajim.svg']), - ('share/icons/hicolor/scalable/apps', - ['gajim/data/icons/hicolor/scalable/apps/org.gajim.Gajim-symbolic.svg']) -] - -data_files: DataFilesT = data_files_app_icon - -setup( - cmdclass={ - 'build_py': Build, - 'install': Install, - }, - data_files=data_files -)
View it on GitLab: https://dev.gajim.org/gajim/gajim/-/compare/326c2c678e2e0861360b793d0fb5198a...