# SPDX-License-Identifier: GPL-3.0-or-later # Definitions of helper commands to use with setuptools # # This file is part of Hydrilla # # Copyright (C) 2021 Wojtek Kosior # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # # I, Wojtek Kosior, thereby promise not to sue for violation of this file's # license. Although I request that you do not make use this code in a # proprietary program, I am not going to enforce this in court. from setuptools import Command from setuptools.command.build_py import build_py import sys from pathlib import Path import subprocess import re import os import json import importlib def mypath(path_or_string): return Path(path_or_string).resolve() debrel_regex = re.compile(r'^[^(]*\([^-]*-([^)]*)\)') def extract_debrel(debian_dir): changelog_path = mypath(debian_dir) / 'changelog' with open(changelog_path) as changelog_file: try: return debrel_regex.match(changelog_file.readline())[1] except TypeError: raise RuntimeException('Cannot extract debrel from %s.' % changelog_path) class Helper: def __init__(self, project_root, app_package_name, version, locales_dir, locales=['en', 'pl'], default_locale='en', locale_domain=None, packages_root=None, debian_dir=None, config_path=None): self.project_root = mypath(project_root) self.app_package_name = app_package_name self.version = version self.locales_dir = mypath(locales_dir) self.locales = locales self.default_locale = default_locale self.locale_domain = locale_domain or app_package_name self.packages_root = mypath(packages_root or project_root / 'src') self.app_package_dir = self.packages_root / app_package_name self.debian_dir = mypath(debian_dir or project_root / 'debian') self.config_path = config_path and mypath(config_path) self.locale_files_list = None def run_command(self, command, verbose, runner=subprocess.run, **kwargs): cwd = kwargs.get('cwd') if cwd: cwd = mypath(cwd) where = f'from {cwd} ' else: cwd = Path.cwd().resolve() where = '' str_command = [str(command[0])] for arg in command[1:]: if isinstance(arg, Path): try: arg = str(arg.relative_to(cwd)) except ValueError: arg = str(arg) str_command.append(arg) if verbose: print(f'{where}executing {" ".join(str_command)}') runner(str_command, **kwargs) def create_mo_files(self, dry_run=False, verbose=False): self.locale_files_list = [] for locale in self.locales: messages_dir = self.locales_dir / locale / 'LC_MESSAGES' for po_path in messages_dir.glob('*.po'): mo_path = po_path.with_suffix('.mo') if not dry_run: command = ['msgfmt', po_path, '-o', mo_path] self.run_command(command, verbose=verbose, check=True) self.locale_files_list.extend([po_path, mo_path]) def locale_files(self): if self.locale_files_list is None: self.create_mo_files(dry_run=True) return self.locale_files_list def locale_files_relative(self, to=None): if to is None: to = self.app_package_dir return [file.relative_to(to) for file in self.locale_files()] def flask_run(self, locale=None): for var, val in (('ENV', 'development'), ('DEBUG', 'True')): os.environ[f'FLASK_{var}'] = os.environ.get(f'FLASK_{var}', val) config = {'lang': locale or self.default_locale} sys.path.insert(0, str(self.packages_root)) package = importlib.import_module(self.app_package_name) # make relative paths in json config resolve from project's directory os.chdir(self.project_root) kwargs = {'config_path': self.config_path} if self.config_path else {} package.create_app(flask_config=config, **kwargs).run() def update_po_files(self, verbose=False): pot_path = self.locales_dir / f'{self.locale_domain}.pot' rglob = self.app_package_dir.rglob command = ['xgettext', '-d', self.locale_domain, '--language=Python', '-o', pot_path, *rglob('*.py'), *rglob('*.html')] self.run_command(command, verbose=verbose, check=True, cwd=self.app_package_dir) for locale in self.locales: messages_dir = self.locales_dir / locale / 'LC_MESSAGES' for po_path in messages_dir.glob('*.po'): if po_path.stem != self.app_package_name: continue; if po_path.exists(): command = ['msgmerge', '--update', po_path, pot_path] else: command = ['cp', po_path, pot_path] self.run_command(command, verbose=verbose, check=True) if (verbose): print('removing generated .pot file') pot_path.unlink() # we exclude these from the source archive we produce bad_file_regex = re.compile(r'^\..*|build|debian|dist') def make_tarballs(self, verbose=False): name=self.app_package_name ver=self.version debrel=extract_debrel(self.debian_dir) source_dirname = f'{name}-{ver}' source_tarball_name = f'{name}_{ver}.orig.tar.gz' debian_tarball_name = f'{name}_{ver}-{debrel}.debian.tar.gz' source_args = [f'--prefix={source_dirname}/', '-o', self.project_root.parent / source_tarball_name, 'HEAD'] for filepath in self.project_root.iterdir(): if not self.bad_file_regex.search(filepath.parts[-1]): source_args.append(filepath) debian_args = ['-o', self.project_root.parent / debian_tarball_name, 'HEAD', self.debian_dir] for args in [source_args, debian_args]: command = ['git', 'archive', '--format=tar.gz', *args] self.run_command(command, verbose=verbose, check=True) def commands(self): helper = self class MsgfmtCommand(Command): '''A custom command to run msgfmt on all .po files below '{}'.''' description = 'use msgfmt to generate .mo files from .po files' user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): helper.create_mo_files(verbose=self.verbose) MsgfmtCommand.__doc__ = MsgfmtCommand.__doc__.format(helper.locales_dir) class RunCommand(Command): ''' A custom command to run the app using flask. This is similar in effect to: PYTHONPATH='{packages_root}' FLASK_APP={app_package_name} \\ FLASK_ENV=development flask run ''' description = 'run the Flask app from source directory' user_options = [ ('locale=', 'l', "app locale (one of: %s; default: '%s')" % (', '.join([f"'{l}'" for l in helper.locales]), helper.default_locale)) ] def initialize_options(self): self.locale = helper.default_locale def finalize_options(self): if self.locale not in helper.locales: raise ValueError("Locale '%s' not supported" % self.lang) def run(self): helper.flask_run(locale=self.locale) RunCommand.__doc__ = RunCommand.__doc__.format( packages_root=self.packages_root, app_package_name=self.app_package_name ) class MsgmergeCommand(Command): ''' A custom command to run xgettext and msgmerge to update project's .po files below '{}'. ''' description = 'use xgettext and msgmerge to update (or generate) .po files for this project' user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): helper.update_po_files(verbose=self.verbose) MsgmergeCommand.__doc__ = \ MsgmergeCommand.__doc__.format(helper.locales_dir) class TarballsCommand(Command): ''' A custom command to run git archive to create debian tarballs of this project. ''' description = 'use git archive to create .orig.tar.gz and .debian.tar.gz files for this project' user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): helper.make_tarballs(verbose=self.verbose) class BuildCommand(build_py): ''' The build command but runs the custom msgfmt command before build. ''' def run(self, *args, **kwargs): self.run_command('msgfmt') super().run(*args, **kwargs) return { 'msgfmt': MsgfmtCommand, 'run': RunCommand, 'msgmerge': MsgmergeCommand, 'tarballs': TarballsCommand, 'build_py': BuildCommand }