From 2c3f95c108394bb41f541cac816eddcb2777efd3 Mon Sep 17 00:00:00 2001 From: David Aguilar Date: Thu, 10 Mar 2022 09:36:38 -0800 Subject: [PATCH] build: eliminate "python setup.py ..." Stop supporting "python setup.py {build,build_pot,build_mo,install}". Handle "make i18n" using the Makefile directly to simplify our dependencies. Stop requiring files that are generated at build-time. Git Cola no longer depends on generated files. Eliminate our dependency on the .mo files and the distutils.commands entry points, which are problem due to the deprecation of distutils. We were doing underhanded things like hooking into the sub_commands in the core setuptools.command.build.build class, which was fragile and did not fully support PEP-517/518 compatible build tools. Furthermore, we were dependent on data_files to install our generated .mo translation files, and there is no current supported method to force "python -m build" or "pip install" to run the .mo generation step during the build. The Python Packaging Authority (PyPA) strongly recommends that *all* data required by the package should be shipped as "package_data". Now that we are using polib we can simply install our .po files directly into the cola package data and read them directly. There is no need to ship binary .mo files or deal with the complexity around them. Related Links: * https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html * https://pypa-build.readthedocs.io/en/stable/installation.html * https://setuptools.pypa.io/en/latest/userguide/datafiles.html Closes #1201 Signed-off-by: David Aguilar --- .github/workflows/main.yml | 5 -- CHANGES.rst | 8 +++ Makefile | 76 ++++++++++++------------ README.md | 3 +- bin/README.md | 5 +- cola/i18n.py | 2 - extras/__init__.py | 9 --- extras/build.py | 8 --- extras/build_mo.py | 119 ------------------------------------- extras/build_pot.py | 142 --------------------------------------------- extras/build_util.py | 39 ------------- extras/install.py | 8 --- setup.cfg | 35 ----------- 13 files changed, 51 insertions(+), 408 deletions(-) delete mode 100644 extras/__init__.py delete mode 100644 extras/build.py delete mode 100644 extras/build_mo.py delete mode 100644 extras/build_pot.py delete mode 100644 extras/build_util.py delete mode 100644 extras/install.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fa28327d..d21f2d7f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -56,22 +56,17 @@ jobs: # Runtime dependencies (optional) sudo apt-get install \ python3-send2trash - python -m venv --system-site-packages env source env/bin/activate - export SETUPTOOLS_USE_DISTUTILS=stdlib make develop - name: Build Translations run: | - source env/bin/activate - export SETUPTOOLS_USE_DISTUTILS=stdlib make i18n - name: Run Unit Tests run: | git config --global user.name "Git Cola" git config --global user.email git-cola@localhost - export SETUPTOOLS_USE_DISTUTILS=stdlib make test - name: Run Linter diff --git a/CHANGES.rst b/CHANGES.rst index 1b49d345..ea4cb37a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -16,6 +16,11 @@ Breaking Changes (`#1204 `_) * The build system was switched to `setuptools` and no longer depends on `distutils`. + ``python setup.py {build,install,build_pot,build_mo}`` are no longer provided. + Use the https://pypa-build.readthedocs.io/en/stable/installation.html + ``python -m build`` tool to generate sdist and wheel distributions, + and ``pip install`` to install Git Cola. + (`#1204 `_) * The `git-cola`, `git-dag` and `git-cola-sequence-editor` commands are now installed using setuptools entry points. @@ -31,6 +36,9 @@ Breaking Changes * The `NO_VENDOR_LIBS` and `NO_PRIVATE_LIBS` Makefile options are no longer necessary. +* The `share/git-cola` filesystem namespace no longer exists. All of cola's package data + is distributed alongside the `cola` module as package data. + Usability, bells and whistles ----------------------------- diff --git a/Makefile b/Makefile index 3a3b0eaf..1ebc3515 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,11 @@ # The default target of this Makefile is... +.PHONY: all all:: # Development # ----------- -# make develop # python setup.py develop ~ one-time virtualenv development setup -# make V=1 # generate files; V=1 increases verbosity +# make V=1 # V=1 increases verbosity +# make develop # pip install --editable . # make test [flags=...] # run tests; flags=-x fails fast, --ff failed first # make test V=2 # V=2 increases test verbosity # make doc # build docs @@ -19,8 +20,7 @@ all:: # ------------ # make pot # update main translation template # make po # merge translations -# make mo # generate message files -# make i18n # all three of the above +# make i18n # make pot + make po # # Installation # ------------ @@ -33,16 +33,13 @@ all:: # # The external commands used by this Makefile are... BLACK = black -CTAGS = ctags CP = cp FIND = find FLAKE8 = flake8 GREP = grep GIT = git -GZIP = gzip -LN = ln -LN_S = $(LN) -s -f MARKDOWN = markdown +MSGMERGE = msgmerge MKDIR_P = mkdir -p PIP = pip PYTHON ?= python @@ -54,6 +51,7 @@ RMDIR = rmdir TAR = tar TOX = tox XARGS = xargs +XGETTEXT = xgettext # Flags # ----- @@ -91,7 +89,6 @@ endif # These values can be overridden on the command-line or via config.mak prefix = $(HOME) -bindir = $(prefix)/bin python_version := $(shell $(PYTHON) -c 'import sys; print("%s.%s" % sys.version_info[:2])') python_lib = python$(python_version)/site-packages pythondir = $(prefix)/lib/$(python_lib) @@ -106,38 +103,27 @@ include cola/_version.py cola_version := $(subst ',,$(VERSION)) cola_dist := $(cola_base)-$(cola_version) -SETUP ?= $(PYTHON) setup.py - install_args = ifdef DESTDIR install_args += --root="$(DESTDIR)" export DESTDIR endif install_args += --prefix="$(prefix)" -install_args += --install-scripts="$(bindir)" -install_args += --single-version-externally-managed -install_args += --record=build/MANIFEST export prefix PYTHON_DIRS = cola PYTHON_DIRS += test ALL_PYTHON_DIRS = $(PYTHON_DIRS) -ALL_PYTHON_DIRS += extras # User customizations -include config.mak -.PHONY: all -all:: build - -.PHONY: build -build:: - $(SETUP) $(QUIET) $(VERBOSE) build +all:: .PHONY: install install:: all - $(SETUP) $(QUIET) $(VERBOSE) install $(install_args) + $(PIP) $(QUIET) $(VERBOSE) install $(install_args) . .PHONY: doc doc:: @@ -176,7 +162,6 @@ uninstall:: $(RM) "$(DESTDIR)$(prefix)"/share/metainfo/git-cola.appdata.xml $(RM) "$(DESTDIR)$(prefix)"/share/icons/hicolor/scalable/apps/git-cola.svg $(RM_R) "$(DESTDIR)$(prefix)"/share/doc/git-cola - $(RM) "$(DESTDIR)$(prefix)"/share/locale/*/LC_MESSAGES/git-cola.mo $(RM_R) "$(DESTDIR)$(pythondir)"/git_cola-* $(RM_R) "$(DESTDIR)$(pythondir)"/cola $(RMDIR) -p "$(DESTDIR)$(pythondir)" 2>/dev/null || true @@ -195,7 +180,7 @@ uninstall:: $(RMDIR) "$(DESTDIR)$(prefix)" 2>/dev/null || true .PHONY: test -test:: all +test:: $(PYTEST) $(PYTEST_FLAGS) $(flags) $(PYTHON_DIRS) .PHONY: coverage @@ -207,26 +192,45 @@ clean:: $(FIND) $(ALL_PYTHON_DIRS) -name '*.py[cod]' -print0 | $(XARGS) -0 $(RM) $(FIND) $(ALL_PYTHON_DIRS) -name __pycache__ -print0 | $(XARGS) -0 $(RM_R) $(RM_R) build dist tags git-cola.app - $(RM_R) share/locale $(MAKE) -C share/doc/git-cola clean # Update i18n files .PHONY: i18n i18n:: pot i18n:: po -i18n:: mo +# Regenerate git-cola.pot with new translations .PHONY: pot pot:: - $(SETUP) build_pot --build-dir=po --no-lang - + $(XGETTEXT) \ + --language=Python \ + --keyword=N_ \ + --no-wrap \ + --no-location \ + --omit-header \ + --sort-output \ + --output-dir cola/i18n \ + --output git-cola.pot \ + cola/*.py \ + cola/*/*.py + +# Update .po files with new translations from git-cola.pot .PHONY: po po:: - $(SETUP) build_pot --build-dir=po - -.PHONY: mo -mo:: - $(SETUP) build_mo --force + for po in cola/i18n/*.po; \ + do \ + $(MSGMERGE) \ + --no-location \ + --no-wrap \ + --no-fuzzy-matching \ + --sort-output \ + --output-file $$po.new \ + $$po \ + cola/i18n/git-cola.pot \ + && \ + mv $$po.new $$po; \ + \ + done .PHONY: git-cola.app git-cola.app:: @@ -287,11 +291,11 @@ format:: $(GREP) -v ^qtpy | \ $(XARGS) $(BLACK) --skip-string-normalization -# Run "make develop" from inside a newly created virtualenv to setup the build system -# using "python setup.py develop". +# Run "make develop" from inside a newly created virtualenv to create an +# editable installation. .PHONY: develop develop:: - $(SETUP) develop + $(PIP) install --editable . .PHONY: requirements requirements:: diff --git a/README.md b/README.md index ef831eeb..e68743d8 100644 --- a/README.md +++ b/README.md @@ -342,8 +342,7 @@ Git Cola installs its modules into the default Python site-packages directory While end-users can use `pip install git-cola` to install Git Cola, distribution packagers should use the `make prefix=/usr` install process. Git Cola's `Makefile` wraps -`python setup.py install --single-version-externally-managed` to provide a -packaging-friendly `make install` target. +`pip install --prefix=` to provide a packaging-friendly `make install` target. # Windows (Continued) diff --git a/bin/README.md b/bin/README.md index 7b381e57..bec3fac2 100644 --- a/bin/README.md +++ b/bin/README.md @@ -1,9 +1,8 @@ # Command-line Wrappers The scripts in this directory are provided for convenience. They allow running -`git-cola` directly from the source tree without needing to use a virtualenv and -`pip install --editable .` or `python setup.py develop` to generate the entry point -scripts. +`git-cola` directly from the source tree without needing to use a virtualenv or +`pip install --editable .` to generate the entry point scripts. The `git cola`, `git cola-rebase-editor` and `git dag` Git sub-commands are provided by the `git-cola`, `git-cola-rebase-editor` and `git-dag` setuptools entry point scripts. diff --git a/cola/i18n.py b/cola/i18n.py index 039148ac..fecd9ef8 100644 --- a/cola/i18n.py +++ b/cola/i18n.py @@ -136,8 +136,6 @@ def _check_win32_locale(): break else: lang = None - import locale # pylint: disable=all - try: import ctypes # pylint: disable=all except ImportError: diff --git a/extras/__init__.py b/extras/__init__.py deleted file mode 100644 index debcdf8a..00000000 --- a/extras/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from __future__ import absolute_import, division, print_function, unicode_literals - -try: - from setuptools.command.build import build as build_base -except ImportError: - try: - from setuptools._distutils.command.build import build as build_base # noqa - except ImportError: - from distutils.command.build import build as build_base # noqa diff --git a/extras/build.py b/extras/build.py deleted file mode 100644 index 9bf4c050..00000000 --- a/extras/build.py +++ /dev/null @@ -1,8 +0,0 @@ -from __future__ import absolute_import, division, print_function, unicode_literals - -from . import build_base as build - - -build.sub_commands = [ - ('build_mo', None), -] + list(build.sub_commands) diff --git a/extras/build_mo.py b/extras/build_mo.py deleted file mode 100644 index 210dcb93..00000000 --- a/extras/build_mo.py +++ /dev/null @@ -1,119 +0,0 @@ -"""build_mo command for setup.py""" -# pylint: disable=attribute-defined-outside-init,import-error,no-name-in-module -from __future__ import absolute_import, division, print_function, unicode_literals -import os -import re - -from setuptools import Command - -from . import build_util -from .build_util import newer -from .build_util import which - - -class build_mo(Command): - """Subcommand of build command: build_mo""" - - description = 'compile po files to mo files' - - # List of options: - # - long name, - # - short name (None if no short name), - # - help string. - user_options = [ - ('build-dir=', 'd', 'Directory to build locale files'), - ('output-base=', 'o', 'mo-files base name'), - ('source-dir=', None, 'Directory with sources po files'), - ('force', 'f', 'Force creation of mo files'), - ('lang=', None, 'Comma-separated list of languages to process'), - ] - user_options = build_util.stringify_options(user_options) - boolean_options = build_util.stringify_list(['force']) - - def initialize_options(self): - self.build_dir = None - self.output_base = None - self.source_dir = None - self.force = None - self.lang = None - - def finalize_options(self): - self.set_undefined_options('build', ('force', 'force')) - self.prj_name = self.distribution.get_name() - if self.build_dir is None: - self.build_dir = os.path.join('share', 'locale') - if not self.output_base: - self.output_base = self.prj_name or 'messages' - if self.source_dir is None: - self.source_dir = 'po' - if self.lang is None: - if self.prj_name: - re_po = re.compile(r'^(?:%s-)?([a-zA-Z_]+)\.po$' % self.prj_name) - else: - re_po = re.compile(r'^([a-zA-Z_]+)\.po$') - self.lang = [] - for i in os.listdir(self.source_dir): - mo = re_po.match(i) - if mo: - self.lang.append(mo.group(1)) - else: - self.lang = [i.strip() for i in self.lang.split(',') if i.strip()] - - def run(self): - """Run msgfmt for each language""" - if not self.lang: - return - - if which('msgfmt') is None: - self.warn('GNU gettext msgfmt utility not found!') - self.warn('Compilation of .po files has been skipped.') - return - - if 'en' in self.lang: - if which('msginit') is None: - self.warn('GNU gettext msginit utility not found!') - self.warn('Skip creating English PO file.') - else: - self.debug_print('Creating English PO file...') - pot = (self.prj_name or 'messages') + '.pot' - if self.prj_name: - en_po = '%s-en.po' % self.prj_name - else: - en_po = 'en.po' - output = os.path.join(self.source_dir, en_po) - self.spawn( - [ - 'msginit', - '--no-translator', - '--no-wrap', - '--locale', - 'en', - '--input', - os.path.join(self.source_dir, pot), - '--output-file', - output, - ] - ) - - basename = self.output_base - if not basename.endswith('.mo'): - basename += '.mo' - - po_prefix = '' - if self.prj_name: - po_prefix = self.prj_name + '-' - for lang in self.lang: - po = os.path.join(self.source_dir, lang + '.po') - if not os.path.isfile(po): - po = os.path.join(self.source_dir, po_prefix + lang + '.po') - dir_ = os.path.join(self.build_dir, lang, 'LC_MESSAGES') - self.mkpath(dir_) - mo = os.path.join(dir_, basename) - if self.force or newer(po, mo): - self.debug_print('Compile: %s -> %s' % (po, mo)) - self.spawn(['msgfmt', '--output-file', mo, po]) - - # pylint: disable=no-self-use - def get_outputs(self): - """The output of this command is handled by the install command""" - return [] diff --git a/extras/build_pot.py b/extras/build_pot.py deleted file mode 100644 index e3fd0e76..00000000 --- a/extras/build_pot.py +++ /dev/null @@ -1,142 +0,0 @@ -"""build_pot command for setup.py""" -# pylint: disable=attribute-defined-outside-init,import-error,no-name-in-module -from __future__ import absolute_import, division, print_function, unicode_literals -import os -import glob - -from setuptools import Command - -from . import build_util - - -class build_pot(Command): - """Distutils command build_pot""" - - description = 'extract strings from python sources for translation' - - # List of options: - # - long name, - # - short name (None if no short name), - # - help string. - user_options = [ - ('build-dir=', 'd', 'Directory to put POT file'), - ('output=', 'o', 'POT filename'), - ('lang=', None, 'Comma-separated list of languages to update po-files'), - ('no-lang', 'N', "Don't update po-files"), - ('english', 'E', 'Regenerate English PO file'), - ] - user_options = build_util.stringify_options(user_options) - boolean_options = build_util.stringify_list(['no-lang', 'english']) - - def initialize_options(self): - self.build_dir = None - self.output = None - self.lang = None - self.no_lang = False - self.english = False - - def finalize_options(self): - if self.build_dir is None: - self.build_dir = 'po' - if not self.output: - self.output = (self.distribution.get_name() or 'messages') + '.pot' - if self.lang is not None: - self.lang = [i.strip() for i in self.lang.split(',') if i.strip()] - if self.lang and self.no_lang: - raise ValueError( - "You can't use options " "--lang=XXX and --no-lang in the same time." - ) - - def run(self): - """Run xgettext for project sources""" - # project name based on `name` argument in setup() call - prj_name = self.distribution.get_name() - # output file - if self.build_dir != '.': - fullname = os.path.join(self.build_dir, self.output) - else: - fullname = self.output - self.debug_print('Generate POT file: ' + fullname) - if not os.path.isdir(self.build_dir): - self.debug_print('Make directory: ' + self.build_dir) - os.makedirs(self.build_dir) - - cmd = [ - 'xgettext', - '--language=Python', - '--keyword=N_', - '--no-wrap', - '--no-location', - '--omit-header', - '--sort-output', - '--output-dir', - self.build_dir, - '--output', - self.output, - ] - cmd.extend(glob.glob('bin/git-*')) - cmd.extend(glob.glob('share/git-cola/bin/git-*')) - cmd.extend(glob.glob('cola/*.py')) - cmd.extend(glob.glob('cola/*/*.py')) - self.spawn(cmd) - - _force_LF(fullname) - # regenerate english PO - if self.english: - self.debug_print('Regenerating English PO file...') - if prj_name: - en_po = prj_name + '-' + 'en.po' - else: - en_po = 'en.po' - self.spawn( - [ - 'msginit', - '--no-translator', - '--locale', - 'en', - '--input', - os.path.join(self.build_dir, self.output), - '--output-file', - os.path.join(self.build_dir, en_po), - ] - ) - # search and update all po-files - if self.no_lang: - return - for po in glob.glob(os.path.join(self.build_dir, '*.po')): - if self.lang is not None: - po_lang = os.path.splitext(os.path.basename(po))[0] - if prj_name and po_lang.startswith(prj_name + '-'): - po_lang = po_lang[5:] - if po_lang not in self.lang: - continue - new_po = po + '.new' - self.spawn( - [ - 'msgmerge', - '--no-location', - '--no-wrap', - '--no-fuzzy-matching', - '--sort-output', - '--output-file', - new_po, - po, - fullname, - ] - ) - # force LF line-endings - self.debug_print('%s --> %s' % (new_po, po)) - _force_LF(new_po, po) - os.unlink(new_po) - - -def _force_LF(src, dst=None): - with open(src, 'rb') as f: - content = f.read().decode('utf-8') - if dst is None: - dst = src - with open(dst, 'wb') as f: - try: - f.write(build_util.encode(content)) - except (IOError, ValueError): - pass diff --git a/extras/build_util.py b/extras/build_util.py deleted file mode 100644 index e14ac715..00000000 --- a/extras/build_util.py +++ /dev/null @@ -1,39 +0,0 @@ -from __future__ import absolute_import, division, print_function, unicode_literals -import os -try: - from shutil import which # pylint: disable=unused-import -except ImportError: - # pylint: disable=unused-import - from distutils.spawn import find_executable as which # noqa - - -def encode(string): - try: - result = string.encode('utf-8') - except (ValueError, UnicodeEncodeError): - result = string - return result - - -def make_string(x): - if x: - x = str(x) - return x - - -def stringify_options(items): - return [[make_string(x) for x in i] for i in items] - - -def stringify_list(items): - return [make_string(i) for i in items] - - -def newer(a, b): - """Return True if a is newer than b""" - try: - stat_a = os.stat(a) - stat_b = os.stat(b) - except OSError: - return True - return stat_a.st_mtime >= stat_b.st_mtime diff --git a/extras/install.py b/extras/install.py deleted file mode 100644 index 1c532cfe..00000000 --- a/extras/install.py +++ /dev/null @@ -1,8 +0,0 @@ -from __future__ import absolute_import, division, print_function, unicode_literals - -from setuptools.command.install import install - - -install.sub_commands = [ - ('build_mo', None), -] + list(install.sub_commands) diff --git a/setup.cfg b/setup.cfg index 9ad513f9..95faedb6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -79,38 +79,6 @@ share/doc/git-cola = share/doc/git-cola/hotkeys_zh_TW.html share/icons/hicolor/scalable/apps = cola/icons/git-cola.svg -share/locale/cs/LC_MESSAGES = - share/locale/cs/LC_MESSAGES/git-cola.mo -share/locale/de/LC_MESSAGES = - share/locale/de/LC_MESSAGES/git-cola.mo -share/locale/es/LC_MESSAGES = - share/locale/es/LC_MESSAGES/git-cola.mo -share/locale/fr/LC_MESSAGES = - share/locale/fr/LC_MESSAGES/git-cola.mo -share/locale/hu/LC_MESSAGES = - share/locale/hu/LC_MESSAGES/git-cola.mo -share/locale/id_ID/LC_MESSAGES = - share/locale/id_ID/LC_MESSAGES/git-cola.mo -share/locale/it/LC_MESSAGES = - share/locale/it/LC_MESSAGES/git-cola.mo -share/locale/ja/LC_MESSAGES = - share/locale/ja/LC_MESSAGES/git-cola.mo -share/locale/pl/LC_MESSAGES = - share/locale/pl/LC_MESSAGES/git-cola.mo -share/locale/pt_BR/LC_MESSAGES = - share/locale/pt_BR/LC_MESSAGES/git-cola.mo -share/locale/ru/LC_MESSAGES = - share/locale/ru/LC_MESSAGES/git-cola.mo -share/locale/sv/LC_MESSAGES = - share/locale/sv/LC_MESSAGES/git-cola.mo -share/locale/tr_TR/LC_MESSAGES = - share/locale/tr_TR/LC_MESSAGES/git-cola.mo -share/locale/uk/LC_MESSAGES = - share/locale/uk/LC_MESSAGES/git-cola.mo -share/locale/zh_CN/LC_MESSAGES = - share/locale/zh_CN/LC_MESSAGES/git-cola.mo -share/locale/zh_TW/LC_MESSAGES = - share/locale/zh_TW/LC_MESSAGES/git-cola.mo [options.extras_require] testing = @@ -145,6 +113,3 @@ console_scripts = git-cola = cola.main:main git-dag = cola.dag:main git-cola-sequence-editor = cola.sequenceeditor:main -# "build" and "install" are overridden to ensure that "build_mo" is always run. -distutils.commands = - build_pot = extras.build_pot:build_pot -- 2.11.4.GIT