diff --git a/HACKING.md b/HACKING.md index 4cec539ec..66860affd 100644 --- a/HACKING.md +++ b/HACKING.md @@ -136,7 +136,7 @@ you will need to run the following to install those files properly on to the system and their changes to reflect properly. ```bash -guest$ sudo ./setup.py install +guest$ sudo make build install ``` Note: This development container has automatic upgrades disabled by default. @@ -367,7 +367,7 @@ you will need to run the following to install those files properly on to the system and their changes to reflect properly. ```bash -vm$ sudo ./setup.py install +vm$ sudo make build install ``` Note: This development virtual machine has automatic upgrades disabled by diff --git a/INSTALL.md b/INSTALL.md index 771354100..19d35cb38 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -24,7 +24,7 @@ FreedomBox [Manual](https://wiki.debian.org/FreedomBox/Manual/)'s install FreedomBox Service (Plinth) itself. ``` - $ sudo python3 setup.py install + $ sudo make build install ``` 2. Run FreedomBox Service (Plinth): diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..e447e25b7 --- /dev/null +++ b/Makefile @@ -0,0 +1,130 @@ +DJANGO_ADMIN := django-admin +INSTALL := install +PYTHON := python3 +PYTEST_ARGS := +CP_ARGS := --no-dereference --preserve=mode,timestamps --reflink=auto + +ENABLED_APPS_PATH := $(DESTDIR)/usr/share/freedombox/modules-enabled + +DISABLED_APPS_TO_REMOVE := \ + apps \ + coquelicot \ + diaspora \ + monkeysphere \ + owncloud \ + system \ + xmpp \ + disks \ + udiskie \ + restore \ + repro \ + tahoe \ + mldonkey + +APP_FILES_TO_REMOVE := $(foreach app,$(DISABLED_APPS_TO_REMOVE),$(ENABLED_APPS_PATH)/$(app)) + +REMOVED_FILES := \ + $(DESTDIR)/etc/apt/preferences.d/50freedombox3.pref \ + $(DESTDIR)/etc/apache2/sites-available/plinth.conf \ + $(DESTDIR)/etc/apache2/sites-available/plinth-ssl.conf \ + $(DESTDIR)/etc/security/access.d/10freedombox-performance.conf \ + $(DESTDIR)/etc/security/access.d/10freedombox-security.conf + +DIRECTORIES_TO_CREATE := \ + $(DESTDIR)/var/lib/plinth \ + $(DESTDIR)/var/lib/plinth/sessions + +STATIC_FILES_DIRECTORY := $(DESTDIR)/usr/share/plinth/static +BIN_DIR := $(DESTDIR)/usr/bin + +FIND_ARGS := \ + -not -iname "*.log" \ + -not -iname "*.pid" \ + -not -iname "*.py.bak" \ + -not -iname "*.pyc" \ + -not -iname "*.pytest_cache" \ + -not -iname "*.sqlite3" \ + -not -iname "*.swp" \ + -not -iname "\#*" \ + -not -iname ".*" \ + -not -iname "sessionid*" \ + -not -iname "~*" \ + -not -iname "django-secret.key" + + +ROOT_DATA_FILES := $(shell find data -type f $(FIND_ARGS)) +MODULE_DATA_FILES := $(shell find $(wildcard plinth/modules/*/data) -type f $(FIND_ARGS)) + +update-translations: + cd plinth; $(DJANGO_ADMIN) makemessages --all --domain django --keep-pot --verbosity=1 + +configure: + # Nothing to do + +build: + # Compile translations + $(DJANGO_ADMIN) compilemessages --verbosity=1 + + # Build documentation + $(MAKE) -C doc -j 8 + + # Build .whl package + $(PYTHON) -m build --no-isolation --skip-dependency-check --wheel + +install: + # Drop removed apps + rm -f $(APP_FILES_TO_REMOVE) + + # Drop removed configuration files + rm -f $(REMOVED_FILES) + + # Create data directories + for directory in $(DIRECTORIES_TO_CREATE) ; do \ + $(INSTALL) -d $$directory ; \ + done + + # Python package + temp=$$(mktemp -d) && \ + lib_dir=$$($(PYTHON) -c 'import sysconfig; print(sysconfig.get_paths(scheme="deb_system")["purelib"])') && \ + $(PYTHON) -m pip install dist/plinth-*.whl --break-system-packages \ + --no-deps --no-compile --no-warn-script-location \ + --ignore-installed --target=$${temp} && \ + $(INSTALL) -d $(DESTDIR)$${lib_dir} && \ + rm -rf $(DESTDIR)$${lib_dir}/plinth $(DESTDIR)$${lib_dir}/plinth*.dist-info && \ + mv $${temp}/plinth $${temp}/plinth*.dist-info $(DESTDIR)$${lib_dir} && \ + rm -f $(DESTDIR)$${lib_dir}/plinth*.dist-info/COPYING.md && \ + rm -f $(DESTDIR)$${lib_dir}/plinth*.dist-info/direct_url.json && \ + $(INSTALL) -D -t $(BIN_DIR) bin/plinth + + # Actions + $(INSTALL) -D -t $(DESTDIR)/usr/share/plinth/actions actions/actions + + # Static web server files + rm -rf $(STATIC_FILES_DIRECTORY) + $(INSTALL) -d $(STATIC_FILES_DIRECTORY) + cp $(CP_ARGS) --recursive static/* $(STATIC_FILES_DIRECTORY) + + # System data files + for file in $(ROOT_DATA_FILES) ; do \ + target=$$(dirname $(DESTDIR)$$(echo $${file} | sed -e 's|^data||')) ; \ + $(INSTALL) --directory --mode=755 $${target} ; \ + cp $(CP_ARGS) $${file} $${target} ; \ + done + for file in $(MODULE_DATA_FILES) ; do \ + target=$$(dirname $(DESTDIR)$$(echo $${file} | sed -e 's|^plinth/modules/[^/]*/data||')) ; \ + $(INSTALL) --directory --mode=755 $${target} ; \ + cp $(CP_ARGS) $${file} $${target} ; \ + done + + # Documentation + $(MAKE) -C doc install + +check: + $(PYTHON) -m pytest $(PYTEST_ARGS) + +clean: + make -C doc clean + rm -rf Plinth.egg-info + find plinth/locale -name *.mo -delete + +.PHONY: update-translations configure build install check clean diff --git a/Vagrantfile b/Vagrantfile index aa5c1b1c7..d25f7c7c8 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -23,7 +23,9 @@ Vagrant.configure(2) do |config| SHELL config.vm.provision "shell", inline: <<-SHELL cd /freedombox/ - ./setup.py install + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get build-dep --no-install-recommends --yes . + make build install systemctl daemon-reload # Stop any ongoing upgrade killall -9 unattended-upgr diff --git a/container b/container index 6da2fcfb6..26fec6f96 100755 --- a/container +++ b/container @@ -166,7 +166,7 @@ if [ $(lsb_release --release --short) != '10' ]; then --no-install-recommends --yes . fi -sudo ./setup.py install +sudo make build install sudo systemctl daemon-reload # In case new dependencies conflict with old dependencies diff --git a/debian/control b/debian/control index af482d8d1..9da9f306e 100644 --- a/debian/control +++ b/debian/control @@ -18,10 +18,12 @@ Build-Depends: e2fsprogs, gir1.2-nm-1.0, libjs-bootstrap4, + pybuild-plugin-pyproject, python3-all:any, python3-apt, python3-augeas, python3-bootstrapform, + python3-build, python3-cherrypy3, python3-configobj, python3-dbus, @@ -38,6 +40,7 @@ Build-Depends: python3-openssl, python3-pampy, python3-paramiko, + python3-pip, python3-psutil, python3-pytest, python3-pytest-cov, diff --git a/debian/freedombox.lintian-overrides b/debian/freedombox.lintian-overrides index 2e3b9ec99..ebbfa5065 100644 --- a/debian/freedombox.lintian-overrides +++ b/debian/freedombox.lintian-overrides @@ -24,3 +24,4 @@ freedombox: package-supports-alternative-init-but-no-init.d-script [usr/lib/syst # Not documentation freedombox: package-contains-documentation-outside-usr-share-doc [usr/share/plinth/static/jslicense.html] +freedombox: package-contains-documentation-outside-usr-share-doc [usr/lib/python3/dist-packages/plinth-*.dist-info/top_level.txt] diff --git a/debian/rules b/debian/rules index 422282dca..1bb50a3f8 100755 --- a/debian/rules +++ b/debian/rules @@ -2,6 +2,12 @@ export DH_VERBOSE=1 export PYBUILD_DESTDIR=debian/tmp +export PYBUILD_SYSTEM=custom +export PYBUILD_CONFIGURE_ARGS=make configure +export PYBUILD_BUILD_ARGS=make PYTHON={interpreter} build +export PYBUILD_INSTALL_ARGS=make PYTHON={interpreter} DESTDIR={destdir} install +export PYBUILD_CLEAN_ARGS=make clean +export PYBUILD_TEST_ARGS=make PYTHON={interpreter} check %: dh $@ --with python3 --buildsystem=pybuild @@ -13,13 +19,6 @@ override_dh_auto_install-indep: # Ensure the list of dependencies is not empty. test -s debian/freedombox.substvars || exit 1 -# pybuild can run pytest. However, when the top level directory is included in -# the path (done using manage.py), it results in import problems. -# https://www.mail-archive.com/debian-python@lists.debian.org/msg17997.html -override_dh_auto_test: - PYBUILD_SYSTEM=custom \ - PYBUILD_TEST_ARGS="{interpreter} -m pytest" dh_auto_test - override_dh_installsystemd: # Do not enable or start any service other than FreedomBox service. Use # of --tmpdir is a hack to workaround an issue with dh_installsystemd diff --git a/doc/Makefile b/doc/Makefile index 614394d1e..95489391b 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -7,7 +7,6 @@ MANUAL_URL="https://wiki.debian.org/{lang-fragment}FreedomBox/Manual?action=show MANUAL_URL_RAW="https://wiki.debian.org/{lang-fragment}FreedomBox/Manual?action=raw" MANUAL_PAGE_URL_RAW="https://wiki.debian.org/{page}?action=raw" -DESTDIR= INSTALL_DIR=$(DESTDIR)/usr/share/freedombox MAN_INSTALL_DIR=$(DESTDIR)/usr/share/man SCRIPTS_DIR=scripts diff --git a/plinth/module_loader.py b/plinth/module_loader.py index cc2a561dd..007fb396f 100644 --- a/plinth/module_loader.py +++ b/plinth/module_loader.py @@ -89,8 +89,8 @@ def _get_modules_enabled_files_to_read(): if module_files: return module_files.values() - # './setup.py install' has not been executed yet. Pickup files to load - # from local module directories. + # 'make build install' has not been executed yet. Pickup files to load from + # local module directories. directory = pathlib.Path(__file__).parent glob_pattern = 'modules/*/data/usr/share/freedombox/modules-enabled/*' return list(directory.glob(glob_pattern)) @@ -124,7 +124,7 @@ def get_module_import_path(module_name: str) -> str: import_path_file = None if not import_path_file: - # './setup.py install' has not been executed yet. Pickup files to load + # 'make build install' has not been executed yet. Pickup files to load # from local module directories. directory = pathlib.Path(__file__).parent import_path_file = (directory / diff --git a/pyproject.toml b/pyproject.toml index 000c94f30..322a0dfff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,8 @@ [project] -name = "FreedomBox" +name = "plinth" description = "A web front end for administering FreedomBox" -author = "FreedomBox Authors" -author_email = "freedombox-discuss@lists.alioth.debian.org" license = {file = "COPYING.md"} +dynamic = ["version"] classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: No Input/Output (Daemon)", @@ -76,6 +75,10 @@ dependencies = [ "ruamel.yaml", ] +[[project.authors]] +name = "FreedomBox Authors" +email = "freedombox-discuss@lists.alioth.debian.org" + [project.optional-dependencies] test = [ "pytest", @@ -95,6 +98,25 @@ changelog = "https://salsa.debian.org/freedombox-team/freedombox/-/blob/master/d readme = "https://salsa.debian.org/freedombox-team/freedombox/-/blob/master/README.md" support = "https://freedombox.org/#community" +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project.scripts] +plinth = "plinth.__main__:main" + +[tool.setuptools.dynamic] +version = {attr = "plinth.__version__"} + +[tool.setuptools.packages.find] +include = ["plinth", "plinth.*"] + +[tool.setuptools.package-data] +"*" = ["templates/*", "static/**", "locale/*/LC_MESSAGES/*.mo"] + +[tool.setuptools.exclude-package-data] +"*" = ["*/data/*"] + [tool.isort] known_first_party = ["plinth"] diff --git a/setup.py b/setup.py deleted file mode 100755 index 00e380280..000000000 --- a/setup.py +++ /dev/null @@ -1,267 +0,0 @@ -#!/usr/bin/python3 -# SPDX-License-Identifier: AGPL-3.0-or-later -""" -FreedomBox Service setup file. - -isort:skip_file -""" - -import collections -import glob -import os -import pathlib -import re -import shutil -import subprocess -import setuptools -from setuptools.command.install import install - -from distutils import log -from distutils.command.build import build -from distutils.command.clean import clean -from distutils.command.install_data import install_data -from distutils.core import Command -from distutils.dir_util import remove_tree -from distutils.util import change_root - -from plinth import __version__ - -DIRECTORIES_TO_CREATE = [ - '/var/lib/plinth', - '/var/lib/plinth/sessions', -] - -DIRECTORIES_TO_COPY = [ - ('/usr/share/plinth/static', 'static'), -] - -ENABLED_APPS_PATH = "/usr/share/freedombox/modules-enabled/" - -DISABLED_APPS_TO_REMOVE = [ - 'apps', - 'coquelicot', - 'diaspora', - 'monkeysphere', - 'owncloud', - 'system', - 'xmpp', - 'disks', - 'udiskie', - 'restore', - 'repro', - 'tahoe', - 'mldonkey', -] - -REMOVED_FILES = [ - '/etc/apt/preferences.d/50freedombox3.pref', - '/etc/apache2/sites-available/plinth.conf', - '/etc/apache2/sites-available/plinth-ssl.conf', - '/etc/security/access.d/10freedombox-performance.conf', - '/etc/security/access.d/10freedombox-security.conf', -] - -LOCALE_PATHS = ['plinth/locale'] - - -class DjangoCommand(Command): - """Setup command to run a Django management command.""" - user_options: list = [] - - def initialize_options(self): - """Declare the options for this command.""" - pass - - def finalize_options(self): - """Declare options dependent on others.""" - pass - - def run(self): - """Execute the command.""" - import django - from django.conf import settings - - settings.configure(LOCALE_PATHS=LOCALE_PATHS) - django.setup() - - # Trick the commands to use the settings properly - os.environ['DJANGO_SETTINGS_MODULE'] = 'x-never-used' - - -class CompileTranslations(DjangoCommand): - """New command to compile .po translation files.""" - description = "compile .po translation files into .mo files" "" - - def run(self): - """Execute the command.""" - DjangoCommand.run(self) - - from django.core.management import call_command - call_command('compilemessages', verbosity=1) - - -class UpdateTranslations(DjangoCommand): - """New command to update .po translation files.""" - description = "update .po translation files from source code" "" - - def run(self): - """Execute the command.""" - DjangoCommand.run(self) - - from django.core.management import call_command - call_command('makemessages', all=True, domain='django', keep_pot=True, - verbosity=1) - - -class CustomBuild(build): - """Override build command to add a subcommand for translations.""" - sub_commands = [('compile_translations', None)] + build.sub_commands - - -class CustomClean(clean): - """Override clean command to clean doc, locales, and egg-info.""" - - def run(self): - """Execute clean command""" - subprocess.check_call(['rm', '-rf', 'Plinth.egg-info/']) - subprocess.check_call(['make', '-C', 'doc', 'clean']) - - for dir_path, dir_names, file_names in os.walk('plinth/locale/'): - for file_name in file_names: - if file_name.endswith('.mo'): - file_path = os.path.join(dir_path, file_name) - log.info("removing '%s'", file_path) - subprocess.check_call(['rm', '-f', file_path]) - - clean.run(self) - - -class CustomInstall(install): - """Override install command.""" - - def run(self): - for app in DISABLED_APPS_TO_REMOVE: - file_path = pathlib.Path(ENABLED_APPS_PATH) / app - if file_path.exists(): - log.info("removing '%s'", str(file_path)) - subprocess.check_call(['rm', '-f', str(file_path)]) - - for path in REMOVED_FILES: - if pathlib.Path(path).exists(): - log.info('removing %s', path) - subprocess.check_call(['rm', '-f', path]) - - install.run(self) - - -class CustomInstallData(install_data): - """Override install command to allow directory creation and copy""" - - def _run_doc_install(self): - """Install documentation""" - command = ['make', '-j', '8', '-C', 'doc', 'install'] - if self.root: - root = os.path.abspath(self.root) - command.append(f'DESTDIR={root}') - - subprocess.check_call(command) - - def run(self): - """Execute install command""" - self._run_doc_install() - - install_data.run(self) # Old style base class - - # Create empty directories - for directory in DIRECTORIES_TO_CREATE: - if self.root: - directory = change_root(self.root, directory) - - if not os.path.exists(directory): - log.info("creating directory '%s'", directory) - os.makedirs(directory) - - # Recursively overwrite directories - for target, source in DIRECTORIES_TO_COPY: - if self.root: - target = change_root(self.root, target) - - if os.path.exists(target): - remove_tree(target) - - log.info("recursive copy '%s' to '%s'", source, target) - shutil.copytree(source, target, symlinks=True) - - -def _ignore_data_file(file_name): - """Ignore common patterns in data files and directories.""" - ignore_patterns = [ - r'\.log$', r'\.pid$', r'\.py.bak$', r'\.pyc$', r'\.pytest_cache$', - r'\.sqlite3$', r'\.swp$', r'^#', r'^\.', r'^__pycache__$', - r'^sessionid\w*$', r'~$', r'django-secret.key' - ] - for pattern in ignore_patterns: - if re.match(pattern, file_name): - return True - - return False - - -def _gather_data_files(): - """Return a list data files are required by setuptools.setup(). - - - Automatically infer the target directory by looking at the relative path - of a file. - - - Allow each app to have it's own folder for data files. - - - Ignore common backup files. - - """ - data_files = collections.defaultdict(list) - crawl_directories = ['data'] - with os.scandir('plinth/modules/') as iterator: - for entry in iterator: - if entry.is_dir(): - crawl_directories.append(os.path.join(entry.path, 'data')) - - for crawl_directory in crawl_directories: - crawl_directory = crawl_directory.rstrip('/') - for path, _, file_names in os.walk(crawl_directory): - target_directory = path[len(crawl_directory):] - if _ignore_data_file(os.path.basename(path)): - continue - - for file_name in file_names: - if _ignore_data_file(file_name): - continue - - data_files[target_directory].append( - os.path.join(path, file_name)) - - return list(data_files.items()) - - -find_packages = setuptools.PEP420PackageFinder.find -setuptools.setup( - version=__version__, - packages=find_packages(include=['plinth', 'plinth.*'], - exclude=['*.templates']), - scripts=['bin/plinth'], - package_data={ - '': ['templates/*', 'static/**', 'locale/*/LC_MESSAGES/*.mo'] - }, - exclude_package_data={'': ['*/data/*']}, - data_files=_gather_data_files() + - [('/usr/share/plinth/actions', glob.glob(os.path.join('actions', - '[a-z]*'))), - ('/usr/share/man/man1', ['doc/plinth.1'])], - cmdclass={ - 'install': CustomInstall, - 'build': CustomBuild, - 'clean': CustomClean, - 'compile_translations': CompileTranslations, - 'install_data': CustomInstallData, - 'update_translations': UpdateTranslations, - }, -)