From 949f88a0dddbda4a8109a48ceacf675f65825766 Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Tue, 15 Nov 2022 14:21:20 -0500 Subject: [PATCH] RPM Package Compatibility Fix (#757) --- .gitignore | 1 + Dockerfile.el8 | 36 ++++++ Dockerfile.el9 | 36 ++++++ Dockerfile.f37 | 33 ++++++ bin/README.md | 14 +++ bin/build-rpm.sh | 110 ++++++++++++++++++ docker-compose.yml | 33 ++++++ .../redhat/apprise-no-macosx-testing.patch | 12 ++ ...pprise-pytest-session_mocker-removal.patch | 19 +++ packaging/redhat/python-apprise.spec | 29 +++++ setup.cfg | 2 +- test/test_plugin_mqtt.py | 2 +- test/test_plugin_syslog.py | 10 +- 13 files changed, 330 insertions(+), 7 deletions(-) create mode 100644 Dockerfile.el8 create mode 100644 Dockerfile.el9 create mode 100644 Dockerfile.f37 create mode 100755 bin/build-rpm.sh create mode 100644 packaging/redhat/apprise-no-macosx-testing.patch create mode 100644 packaging/redhat/apprise-pytest-session_mocker-removal.patch diff --git a/.gitignore b/.gitignore index f4d608a8..b7ccab0e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ __pycache__/ env/ .venv* build/ +BUILDROOT/ develop-eggs/ dist/ downloads/ diff --git a/Dockerfile.el8 b/Dockerfile.el8 new file mode 100644 index 00000000..002f0a41 --- /dev/null +++ b/Dockerfile.el8 @@ -0,0 +1,36 @@ +# Base +FROM rockylinux:8 +ENV container docker +RUN ( \ + cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == \ + systemd-tmpfiles-setup.service ] || rm -f $i; done); \ + rm -f /lib/systemd/system/multi-user.target.wants/*;\ + rm -f /etc/systemd/system/*.wants/*;\ + rm -f /lib/systemd/system/local-fs.target.wants/*; \ + rm -f /lib/systemd/system/sockets.target.wants/*udev*; \ + rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \ + rm -f /lib/systemd/system/basic.target.wants/*;\ + rm -f /lib/systemd/system/anaconda.target.wants/*; \ + echo "assumeyes=1" >> /etc/yum.conf; \ + dnf install -y epel-release; \ + dnf install -y rpm-build rpmlint python3-pip python3-virtualenv rubygem-ronn \ + dnf-plugins-core 'dnf-command(config-manager)' \ + 'dnf-command(builddep)' sudo rsync rpmdevtools; \ + dnf config-manager --set-enabled powertools; + +COPY packaging/redhat/python-apprise.spec / +# Place our build file into the path +COPY bin/build-rpm.sh /usr/bin +RUN rpmspec -q --buildrequires /python-apprise.spec | cut -f1 -d' ' | \ + xargs dnf install -y + +# RPM Build Structure Setup +ENV FLAVOR=rpmbuild OS=centos DIST=el8 +RUN useradd builder -u 1000 -m -G users,wheel &>/dev/null && \ + echo "builder ALL=(ALL:ALL) NOPASSWD:ALL" >> /etc/sudoers + +VOLUME ["/apprise"] +WORKDIR /apprise + +# RPMs should never be built as root +USER builder diff --git a/Dockerfile.el9 b/Dockerfile.el9 new file mode 100644 index 00000000..38a8d477 --- /dev/null +++ b/Dockerfile.el9 @@ -0,0 +1,36 @@ +# Base +FROM rockylinux:9 +ENV container docker +RUN ( \ + cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == \ + systemd-tmpfiles-setup.service ] || rm -f $i; done); \ + rm -f /lib/systemd/system/multi-user.target.wants/*;\ + rm -f /etc/systemd/system/*.wants/*;\ + rm -f /lib/systemd/system/local-fs.target.wants/*; \ + rm -f /lib/systemd/system/sockets.target.wants/*udev*; \ + rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \ + rm -f /lib/systemd/system/basic.target.wants/*;\ + rm -f /lib/systemd/system/anaconda.target.wants/*; \ + echo "assumeyes=1" >> /etc/yum.conf; \ + dnf install -y epel-release; \ + dnf install -y rpm-build rpmlint python3-pip rubygem-ronn \ + dnf-plugins-core 'dnf-command(config-manager)' \ + 'dnf-command(builddep)' sudo rsync rpmdevtools; \ + dnf config-manager --set-enabled crb; + +COPY packaging/redhat/python-apprise.spec / +# Place our build file into the path +COPY bin/build-rpm.sh /usr/bin +RUN rpmspec -q --buildrequires /python-apprise.spec | cut -f1 -d' ' | \ + xargs dnf install -y + +# RPM Build Structure Setup +ENV FLAVOR=rpmbuild OS=centos DIST=el8 +RUN useradd builder -u 1000 -m -G users,wheel &>/dev/null && \ + echo "builder ALL=(ALL:ALL) NOPASSWD:ALL" >> /etc/sudoers + +VOLUME ["/apprise"] +WORKDIR /apprise + +# RPMs should never be built as root +USER builder diff --git a/Dockerfile.f37 b/Dockerfile.f37 new file mode 100644 index 00000000..58df1cf6 --- /dev/null +++ b/Dockerfile.f37 @@ -0,0 +1,33 @@ +# Base +FROM fedora:37 +ENV container docker +RUN \ + rm -f /usr/lib/systemd/system/multi-user.target.wants/*;\ + rm -f /etc/systemd/system/*.wants/*;\ + rm -f /usr/lib/systemd/system/local-fs.target.wants/*; \ + rm -f /usr/lib/systemd/system/sockets.target.wants/*udev*; \ + rm -f /usr/lib/systemd/system/sockets.target.wants/*initctl*; \ + rm -f /usr/lib/systemd/system/basic.target.wants/*;\ + rm -f /usr/lib/systemd/system/anaconda.target.wants/*; \ + echo "assumeyes=1" >> /etc/dnf/dnf.conf; \ + dnf install -y epel-release; \ + dnf install -y rpm-build rpmlint python3-pip rubygem-ronn \ + dnf-plugins-core 'dnf-command(config-manager)' \ + 'dnf-command(builddep)' sudo rsync rpmdevtools; + +COPY packaging/redhat/python-apprise.spec / +# Place our build file into the path +COPY bin/build-rpm.sh /usr/bin +RUN rpmspec -q --buildrequires /python-apprise.spec | cut -f1 -d' ' | \ + xargs dnf install -y + +# RPM Build Structure Setup +ENV FLAVOR=rpmbuild OS=centos DIST=el8 +RUN useradd builder -u 1000 -m -G users,wheel &>/dev/null && \ + echo "builder ALL=(ALL:ALL) NOPASSWD:ALL" >> /etc/sudoers + +VOLUME ["/apprise"] +WORKDIR /apprise + +# RPMs should never be built as root +USER builder diff --git a/bin/README.md b/bin/README.md index de170e08..5b64e34d 100644 --- a/bin/README.md +++ b/bin/README.md @@ -1,5 +1,6 @@ # Apprise Development Tools +# Common Testing This directory just contains some tools that are useful when developing with Apprise. It is presumed that you've set yourself up with a working development environment before using the tools identified here: ```bash @@ -47,3 +48,16 @@ export PATH="$(pwd)/bin:$PATH" # Now you can call the scripts identified above from anywhere... ``` +## RPM Testing + +Apprise is also packaged for Redhat/Fedora as an RPM. To verify this processs works correctly an additional tool called `build-rpm.sh` is provided. It's best tested using the Docker environments: + ```bash + # To test with el8; do the following: + docker-compose run --rm rpmbuild.el8 build-rpm.sh + + # To test with el9; do the following: + docker-compose run --rm rpmbuild.el9 build-rpm.sh + + # To test with f37; do the following: + docker-compose run --rm rpmbuild.f37 build-rpm.sh + ``` diff --git a/bin/build-rpm.sh b/bin/build-rpm.sh new file mode 100755 index 00000000..749febc6 --- /dev/null +++ b/bin/build-rpm.sh @@ -0,0 +1,110 @@ +#!/bin/bash +# Copyright (C) 2022 Chris Caron +# All rights reserved. +# +# This code is licensed under the MIT License. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files(the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions : +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# Directory where Apprise Source Code can be found +APPRISE_DIR="/apprise" +PYTHON=python3 +PIP=pip3 +VENV_CMD="$PYTHON -m venv" + +mkenv(){ + # Prepares RPM Environment + cat << _EOF > $HOME/.rpmmacros +# macros +%_topdir $APPRISE_DIR +%_sourcedir %{_topdir}/dist +%_specdir %{_topdir}/dist +%_rpmdir %{_topdir}/dist/rpm +%_srcrpmdir %{_topdir}/dist/rpm +%_builddir %{_topdir}/build/rpm +_EOF + # Prepare our working directories if not already present + mkdir -p $APPRISE_DIR/{dist/rpm,build/rpm} + return 0 +} + +clean(){ + # Tidy .pyc files + find $APPRISE_DIR -name '*.pyc' -delete &>/dev/null + find $APPRISE_DIR -type d -name '__pycache__' -exec rm -rf {} \ &>/dev/null; + # Remove previously build details + [ -d "$APPRISE_DIR/apprise.egg-info" ] && rm -rf $APPRISE_DIR/apprise.egg-info + [ -d "$APPRISE_DIR/build" ] && rm -rf $APPRISE_DIR/build + [ -d "$APPRISE_DIR/BUILDROOT" ] && rm -rf $APPRISE_DIR/BUILDROOT +} + +build(){ + # Test spec file for any issues + rpmlint "$APPRISE_DIR/packaging/redhat/python-apprise.spec" + [ $? -ne 0 ] && echo "RPMLint Failed!" && return 1 + + # Prepare RPM Package + # Detect our version + local VER=$(rpmspec -q --qf "%{version}\n" \ + "$APPRISE_DIR/packaging/redhat/python-apprise.spec" 2>/dev/null | head -n1) + [ -z "$VER" ] && echo "Could not detect Apprise RPM Version" && return 1 + + if [ ! -f "$APPRISE_DIR/dist/apprise-$VER.tar.gz" ]; then + # Build Apprise + if [ ! -x $HOME/dev/bin/activate ]; then + $VENV_CMD $HOME/dev + [ $? -ne 0 ] && echo "Could not create Virtual Python Environment" && return 1 + fi + . $HOME/dev/bin/activate + $PIP install coverage babel wheel markdown + + pushd $APPRISE_DIR + # Build Man Page + ronn --roff $APPRISE_DIR/packaging/man/apprise.md + $PYTHON setup.py extract_messages + $PYTHON setup.py sdist + + # exit from our virtual environment + deactivate + fi + + # Prepare our RPM Source and SPEC dependencies + find "$APPRISE_DIR/packaging/man/" -type f -name '*.1' \ + -exec cp --verbose {} "$APPRISE_DIR/dist" \; + find "$APPRISE_DIR/packaging/redhat" -type f -name '*.patch' \ + -exec cp --verbose {} "$APPRISE_DIR/dist" \; + find "$APPRISE_DIR/packaging/redhat" -type f -name '*.spec' \ + -exec cp --verbose {} "$APPRISE_DIR/dist" \; + + # Build and Test our RPM Package + rpmbuild -ba "$APPRISE_DIR/dist/python-apprise.spec" + return $? +} + +# Prepare our environment +mkenv + +# Clean +clean + +# Build +build + +# Return our build status +exit $? diff --git a/docker-compose.yml b/docker-compose.yml index 43d928a1..f2d22f05 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,28 @@ services: volumes: - ./:/apprise + rpmbuild.el8: + build: + context: . + dockerfile: Dockerfile.el8 + volumes: + - ./:/apprise + + rpmbuild.el9: + build: + context: . + dockerfile: Dockerfile.el9 + volumes: + - ./:/apprise + + rpmbuild.f37: + build: + context: . + dockerfile: Dockerfile.f37 + volumes: + - ./:/apprise + + # Connect to web and create a new project using the manage script # -> docker-compose run --rm test.py36 bash # bin/apprise - @@ -33,3 +55,14 @@ services: # # # Now produce a report # docker-compose run --rm test.py310 coverage report --show-missing + +# +# RPM Building +# + +# el8 +# - docker-compose run --rm rpmbuild.el8 build-rpm.sh +# el9 +# - docker-compose run --rm rpmbuild.el9 build-rpm.sh +# f37 (Fedora) +# - docker-compose run --rm rpmbuild.f37 build-rpm.sh diff --git a/packaging/redhat/apprise-no-macosx-testing.patch b/packaging/redhat/apprise-no-macosx-testing.patch new file mode 100644 index 00000000..59aa9221 --- /dev/null +++ b/packaging/redhat/apprise-no-macosx-testing.patch @@ -0,0 +1,12 @@ +diff -Naur apprise-1.2.0/test/test_plugin_macosx.py apprise-1.2.0.patched/test/test_plugin_macosx.py +--- apprise-1.2.0/test/test_plugin_macosx.py 2022-11-02 14:59:59.000000000 -0400 ++++ apprise-1.2.0.patched/test/test_plugin_macosx.py 2022-11-15 13:25:27.991284405 -0500 +@@ -38,7 +38,7 @@ + logging.disable(logging.CRITICAL) + + +-if sys.platform not in ["darwin", "linux"]: ++if sys.platform not in ["darwin"]: + pytest.skip("Only makes sense on macOS, but also works on Linux", + allow_module_level=True) + diff --git a/packaging/redhat/apprise-pytest-session_mocker-removal.patch b/packaging/redhat/apprise-pytest-session_mocker-removal.patch new file mode 100644 index 00000000..03bcc2b2 --- /dev/null +++ b/packaging/redhat/apprise-pytest-session_mocker-removal.patch @@ -0,0 +1,19 @@ +diff -Naur apprise-1.2.0/test/conftest.py apprise-1.2.0.patched/test/conftest.py +--- apprise-1.2.0/test/conftest.py 2022-11-02 14:00:13.000000000 -0400 ++++ apprise-1.2.0.patched/test/conftest.py 2022-11-15 10:28:23.793969418 -0500 +@@ -32,12 +32,12 @@ + sys.path.append(os.path.join(os.path.dirname(__file__), 'helpers')) + + +-@pytest.fixture(scope="session", autouse=True) +-def no_throttling_everywhere(session_mocker): ++@pytest.fixture(autouse=True) ++def no_throttling_everywhere(mocker): + """ + A pytest session fixture which disables throttling on all notifiers. + It is automatically enabled. + """ + for notifier in NOTIFY_MODULE_MAP.values(): + plugin = notifier["plugin"] +- session_mocker.patch.object(plugin, "request_rate_per_sec", 0) ++ mocker.patch.object(plugin, "request_rate_per_sec", 0) diff --git a/packaging/redhat/python-apprise.spec b/packaging/redhat/python-apprise.spec index 1eae7512..2cb79787 100644 --- a/packaging/redhat/python-apprise.spec +++ b/packaging/redhat/python-apprise.spec @@ -54,10 +54,25 @@ Summary: A simple wrapper to many popular notification services used toda License: MIT URL: https://github.com/caronc/%{pypi_name} Source0: %{url}/archive/v%{version}/%{pypi_name}-%{version}.tar.gz + # RHEL/Rocky 8 ship with Click v6.7 which does not support the .stdout # directive used in the unit testing. This patch just makes it so our package # continues to be compatible with these linux distributions Patch0: %{pypi_name}-click67-support.patch + +# RHEL/Rocky 8 ship with Pytest v3.4.2 which does not support the +# session_mocker fixture. This patch removes the session_mocker +# Patch thanks to Andreas Motl and his PR: +# - https://github.com/caronc/apprise/pull/763 +Patch1: %{pypi_name}-pytest-session_mocker-removal.patch + +# RHEL/Rocky 8 ship with Pytest v3.4.2 which does not support the +# tmp_path fixture. This patch removes the macos testing as it +# leverages this unavailabe fixture. +# At the end of the day, the macos testing it is not needed by a +# RHEL/Fedora environment anyway for obvious reasons. +Patch2: %{pypi_name}-no-macosx-testing.patch + BuildArch: noarch %description %{common_description} @@ -86,11 +101,18 @@ BuildRequires: python%{python3_pkgversion}-markdown BuildRequires: python%{python3_pkgversion}-yaml BuildRequires: python%{python3_pkgversion}-babel BuildRequires: python%{python3_pkgversion}-cryptography +BuildRequires: python%{python3_pkgversion}-paho-mqtt Requires: python%{python3_pkgversion}-requests Requires: python%{python3_pkgversion}-requests-oauthlib Requires: python%{python3_pkgversion}-markdown Requires: python%{python3_pkgversion}-cryptography Requires: python%{python3_pkgversion}-yaml +Recommends: python%{python3_pkgversion}-paho-mqtt + +%if 0%{?rhel} && 0%{?rhel} <= 8 +BuildRequires: python%{python3_pkgversion}-dataclasses +Requires: python%{python3_pkgversion}-dataclasses +%endif %if %{with tests} %if 0%{?rhel} >= 9 @@ -100,7 +122,10 @@ Requires: python%{python3_pkgversion}-yaml BuildRequires: python%{python3_pkgversion}-mock %endif BuildRequires: python%{python3_pkgversion}-pytest +BuildRequires: python%{python3_pkgversion}-pytest-mock BuildRequires: python%{python3_pkgversion}-pytest-runner +BuildRequires: python%{python3_pkgversion}-pytest-cov +BuildRequires: python%{python3_pkgversion}-pytest-xdist %endif %description -n python%{python3_pkgversion}-%{pypi_name} %{common_description} @@ -110,6 +135,10 @@ BuildRequires: python%{python3_pkgversion}-pytest-runner %if 0%{?rhel} && 0%{?rhel} <= 8 # Rocky/RHEL 8 click v6.7 unit testing support %patch0 -p1 +# Rocky/RHEL 8 Drop session_mocker support +%patch1 -p1 +# Rocky/RHEL 8 Lose MacOSX Testing +%patch2 -p1 %endif %if 0%{?rhel} >= 9 diff --git a/setup.cfg b/setup.cfg index 2aa4146c..c2f20940 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,7 +16,7 @@ builtins = _ test=pytest [tool:pytest] -addopts = --verbosity=3 -ra +addopts = -ra python_files = test/test_*.py norecursedirs=test/helpers filterwarnings = diff --git a/test/test_plugin_mqtt.py b/test/test_plugin_mqtt.py index bb9f819b..ecc13acf 100644 --- a/test/test_plugin_mqtt.py +++ b/test/test_plugin_mqtt.py @@ -45,7 +45,7 @@ def mqtt_client_mock(mocker): """ if "paho" not in sys.modules: - raise pytest.skip(reason="Requires that `paho-mqtt` is installed") + raise pytest.skip("Requires that `paho-mqtt` is installed") # Establish mock of the `publish()` response object. publish_result = Mock(**{ diff --git a/test/test_plugin_syslog.py b/test/test_plugin_syslog.py index 6efb3f45..4d343cd9 100644 --- a/test/test_plugin_syslog.py +++ b/test/test_plugin_syslog.py @@ -24,6 +24,7 @@ # THE SOFTWARE. import re +import sys import pytest from unittest import mock @@ -34,12 +35,11 @@ import socket import logging logging.disable(logging.CRITICAL) +# Skip tests when Python environment does not provide the `syslog` package. +if 'syslog' not in sys.modules: + pytest.skip("Skipping syslog based tests", allow_module_level=True) -# The `syslog` module is not available on Windows. -# `ModuleNotFoundError: No module named 'syslog'` -NotifySyslog = pytest.importorskip( - "apprise.plugins.NotifySyslog", - reason="`syslog` module not available on Windows").NotifySyslog +from apprise.plugins.NotifySyslog import NotifySyslog # noqa E402 @mock.patch('syslog.syslog')