From 54f3c761701122ef6a4846ce83a4307e0ebc26f7 Mon Sep 17 00:00:00 2001 From: Dana Hynes <46582023+danahynes@users.noreply.github.com> Date: Sun, 20 Nov 2022 15:37:08 -0500 Subject: [PATCH] Merge remote-tracking branch 'origin/release' --- .gitignore | 181 +++++++++ .vscode/settings.json | 3 - LICENSE | 13 + MANIFEST.in | 9 + README.md | 117 ++++++ _root/run_as_root.py | 21 -- install.json | 27 -- install.py | 480 ------------------------ pyproject.toml | 42 +++ requirements.txt | 11 + src/installerator/__init__.py | 15 + src/installerator/base_installerator.py | 209 +++++++++++ src/installerator/installerator.py | 229 +++++++++++ src/installerator/uninstallerator.py | 149 ++++++++ tests/install.py | 50 +++ tests/uninstall.py | 34 ++ todo/checklist.txt | 52 +++ todo/howto.txt | 80 ++++ todo/run_as_root.txt | 18 + todo/todo.txt | 7 + uninstall.py | 243 ------------ 21 files changed, 1216 insertions(+), 774 deletions(-) create mode 100644 .gitignore delete mode 100644 .vscode/settings.json create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.md delete mode 100644 _root/run_as_root.py delete mode 100644 install.json delete mode 100755 install.py create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 src/installerator/__init__.py create mode 100644 src/installerator/base_installerator.py create mode 100755 src/installerator/installerator.py create mode 100755 src/installerator/uninstallerator.py create mode 100644 tests/install.py create mode 100644 tests/uninstall.py create mode 100644 todo/checklist.txt create mode 100644 todo/howto.txt create mode 100644 todo/run_as_root.txt create mode 100644 todo/todo.txt delete mode 100755 uninstall.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e8fbf6e --- /dev/null +++ b/.gitignore @@ -0,0 +1,181 @@ +# ------------------------------------------------------------------------------ +# Filename: .gitignore / \ +# Project : Installerator | () | +# Date : 10/28/2022 | | +# Author : cyclopticnerve | \____/ | +# License : WTFPLv2 \ / +# ------------------------------------------------------------------------------ + +# shamelessly stolen from +# https://github.com/github/gitignore/blob/main/Python.gitignore +# (and reformatted) + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the +# code is intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in +# version control. +# However, in case of collaboration, if having platform-specific dependencies +# or dependencies having no cross-platform support, pipenv may install +# dependencies that don't work, or not install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock +# in version control. +# This is especially recommended for binary packages to ensure +# reproducibility, and is more commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in +# version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended +# to not include it in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. +# github.com/David-OConnor/pyflow and github.com/pdm-project/pdm + +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore +# that can be found at +# https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a +# more nuclear option (not recommended) you can uncomment the following to +# ignore the entire idea folder. +#.idea/ + +# -) diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 221edb8..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "todo-tree.tree.scanMode": "workspace only" -} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ad1989f --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..7489452 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,9 @@ +# ------------------------------------------------------------------------------ +# Filename: MANIFEST.in / \ +# Project : Installerator | () | +# Date : 10/28/2022 | | +# Author : cyclopticnerve | \____/ | +# License : WTFPLv2 \ / +# ------------------------------------------------------------------------------ + +# -) diff --git a/README.md b/README.md new file mode 100644 index 0000000..f98629b --- /dev/null +++ b/README.md @@ -0,0 +1,117 @@ + + + + + + + + +# Installerator + +## "It mostly works™" + +[![License: WTFPL](https://img.shields.io/badge/License-WTFPL-brightgreen.svg)](http://www.wtfpl.net/about/) + +A small Python module that makes working with json settings files easier + +## Installing + +You can download the (hopefully stable) +[latest release](https://github.com/cyclopticnerve/installerator/releases/latest) +from the main branch.
+Download the Source Code (tar.gz) file.
+Then install it using: +```bash +foo@bar:~$ cd Downloads +foo@bar:~/Downloads$ python -m pip install Installerator-X.X.X.tar.gz +``` + +Or you can clone the git repo to get the latest (and often broken) code from the dev branch: +```bash +foo@bar:~$ cd Downloads +foo@bar:~/Downloads$ git clone https://github.com/cyclopticnerve/Installerator +foo@bar:~/Downloads$ cd Installerator +foo@bar:~/Downloads/Installerator$ python -m build +foo@bar:~/Downloads/Installerator$ python -m pip install ./dist/installerator-X.X.X.tar.gz -r ./requirements.txt +``` + +## Requirements + +This package relies on another package, Configurator. +Find out more about this package [here](https://github.com/cyclopticnerve/configurator). + +## Uninstalling + +```python +python -m pip uninstall installerator +``` + +## Usage + +### Install: +```python +# NB: the whole import thing is still hazy to me, but this works 100% +# from . import +from installerator.installerator import Installerator + +# the user dict +dict_user = { + "general": { + "name": "SpaceOddity" + }, + "py_reqs": [ + "python-crontab" + ], + "dirs": [ + "${HOME}/.spaceoddity", + "${HOME}/.config/spaceoddity" + ], + "files": { + "${SRC}/spaceoddity.py": "${HOME}/.spaceoddity", + "${SRC}/LICENSE": "${HOME}/.spaceoddity", + "${SRC}/VERSION": "${HOME}/.spaceoddity", + "${SRC}/uninstall.py": "${HOME}/.spaceoddity", + "${SRC}/uninstall.json": "${HOME}/.spaceoddity", + "${SRC}/cron_uninstall.py": "${HOME}/.spaceoddity" + }, + "postflight": [ + "${SRC}/convert_json.py", + "${SRC}/cron_install.py", + "${HOME}/.spaceoddity/spaceoddity.py" + ] +} + +# create an instance of the class +inst = Installerator() + +# # run the instance +inst.run(dict_user) +``` + +### Uninstall: +```python +# NB: the whole import thing is still hazy to me, but this works 100% +# from . import +from installerator.uninstallerator import Uninstallerator + +dict_user = { + "general": { + "name": "SpaceOddity" + }, + "preflight": [ + "${HOME}/.spaceoddity/cron_uninstall.py" + ], + "dirs": [ + "${HOME}/.spaceoddity", + "${HOME}/.config/spaceoddity" + ] +} + +# create an instance of the class +uninst = Uninstallerator() + +# # run the instance +uninst.run(dict_user) +``` + +## -) diff --git a/_root/run_as_root.py b/_root/run_as_root.py deleted file mode 100644 index d9fd1ed..0000000 --- a/_root/run_as_root.py +++ /dev/null @@ -1,21 +0,0 @@ -'general': { - 'run_as_root': 0, - 'prog_name': '', - 'disp_name': '' - }, - - - # check for run as root/need to run as root - run_as_root = self.conf_dict['general']['run_as_root'] - file_name = os.path.basename(__file__) - run_root = (os.geteuid() == 0) - if run_as_root and not run_root: - msg = 'This script needs to be run as root. '\ - f'Try \'sudo ./{file_name}\'' - print(msg) - exit() - elif not run_as_root and run_root: - msg = 'This script should not be run as root. '\ - f'Try \'./{file_name}\'' - print(msg) - exit() diff --git a/install.json b/install.json deleted file mode 100644 index 6c8f28c..0000000 --- a/install.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "general": { - "prog_name": "SpaceOddity" - }, - "preflight": [ - ], - "sys_reqs": [ - ], - "py_reqs": [ - "python-crontab" - ], - "dirs": [ - "${HOME}/.spaceoddity", - "${HOME}/.config/spaceoddity" - ], - "files": { - "spaceoddity.py": "${HOME}/.spaceoddity", - "uninstall.py": "${HOME}/.spaceoddity", - "LICENSE": "${HOME}/.spaceoddity", - "VERSION": "${HOME}/.spaceoddity" - }, - "postflight": [ - "convert_json.py", - "cron_install.py", - "${HOME}/.spaceoddity/spaceoddity.py" - ] -} diff --git a/install.py b/install.py deleted file mode 100755 index 649cb75..0000000 --- a/install.py +++ /dev/null @@ -1,480 +0,0 @@ -#!/usr/bin/env python3 -# -----------------------------------------------------------------------------# -# Filename: install.py / \ # -# Project : Installer | () | # -# Date : 09/29/2022 | | # -# Author : Dana Hynes | \____/ | # -# License : WTFPLv2 \ / # -# -----------------------------------------------------------------------------# - -# NEXT: cmdline option for path to config file -# NEXT: less output, only print step name and ... Done -# NEXT: pre/postflight exit codes -# NEXT: load_conf should load vars from json file - -# NEXT: unattended install to get rid of messages - need to select [Y/n] -# NEXT: how to check results of apt and pip install -# use subprocess.call and check result - see main script -# NEXT: redirect apt and pip to /dev/null to reduce messages -# NEXT: make a module with installer/uninstaller -# NEXT: add version string to options and print at start of install - -# ------------------------------------------------------------------------------ -# Imports -# ------------------------------------------------------------------------------ - -import json -import os -import shlex -import shutil -import subprocess - -# ------------------------------------------------------------------------------ -# Constants -# ------------------------------------------------------------------------------ - -DEBUG = 1 - -# ------------------------------------------------------------------------------ -# Define the main class -# ------------------------------------------------------------------------------ - - -class Installer: - - # -------------------------------------------------------------------------- - # Methods - # -------------------------------------------------------------------------- - - # -------------------------------------------------------------------------- - # Initialize the class - # -------------------------------------------------------------------------- - def __init__(self): - - # get locations - self.src_dir = os.path.dirname(os.path.abspath(__file__)) - self.conf_path = os.path.join(self.src_dir, 'install.json') - - # set default config dict - self.conf_dict_def = { - 'general': { - 'prog_name': '' - }, - 'preflight': [ - ], - 'sys_reqs': [ - ], - 'py_reqs': [ - ], - 'dirs': [ - ], - 'files': { - }, - 'postflight': [ - ] - } - - # user config dict (set to defaults before trying to load file) - self.conf_dict = self.conf_dict_def.copy() - - # -------------------------------------------------------------------------- - # Run the script - # -------------------------------------------------------------------------- - def run(self): - - # init the config dict from user settings - self.__load_conf() - - # substitute ${HOME} in config file - self.__make_subs() - - # check if we need sudo password - self.__check_sudo() - - # show some text - prog_name = self.conf_dict['general']['prog_name'] - print(f'Installing {prog_name}') - - # do the steps in order - self.do_preflight() - self.do_sys_reqs() - self.do_py_reqs() - self.do_dirs() - self.do_files() - self.do_postflight() - - # done installing - print(f'{prog_name} installed') - - # -------------------------------------------------------------------------- - # Steps - # -------------------------------------------------------------------------- - - # -------------------------------------------------------------------------- - # Run preflight scripts - # -------------------------------------------------------------------------- - - def do_preflight(self): - - # check for empty/no list - if not (self.__needs_step('preflight')): - return - - # show some text - print('Running preflight scripts') - - for item in self.conf_dict['preflight']: - - # show that we are doing something - print(f'Running {item}') - - # make relative file into absolute - abs_item = os.path.join(self.src_dir, item) - - # run item - cmd = abs_item - cmd_array = shlex.split(cmd) - try: - if not DEBUG: - subprocess.run(cmd_array) - else: - print(f'DEBUG: {cmd}') - except Exception as error: - print(f'Could not run {cmd}: {error}') - exit() - - # -------------------------------------------------------------------------- - # Install system prerequisites - # -------------------------------------------------------------------------- - def do_sys_reqs(self): - - # check for empty/no list - if not (self.__needs_step('sys_reqs')): - return - - # show some text - print('Installing system requirements') - - # get system requirements - for item in self.conf_dict['sys_reqs']: - - # show that we are doing something - print(f'Installing {item}') - - # install apt reqs - cmd = f'sudo apt-get install {item}' - cmd_array = shlex.split(cmd) - try: - if not DEBUG: - cp = subprocess.run(cmd_array) - cp.check_returncode() - else: - print(f'DEBUG: {cmd}') - except Exception as error: - print(f'Could not install {item}: {error}') - exit() - - # -------------------------------------------------------------------------- - # Install python prerequisites - # -------------------------------------------------------------------------- - def do_py_reqs(self): - - # check for empty/no list - if not (self.__needs_step('py_reqs')): - return - - # show some text - print('Installing python requirements') - - # show that we are doing something - print('Installing pip') - - # always install pip - cmd = 'sudo apt-get install python3-pip' - cmd_array = shlex.split(cmd) - try: - if not DEBUG: - cp = subprocess.run(cmd_array) - cp.check_returncode() - else: - print(f'DEBUG: {cmd}') - except Exception as error: - print(f'Could not install pip: {error}') - exit() - - # get python requirements - for item in self.conf_dict['py_reqs']: - - # show that we are doing something - print(f'Installing {item}') - - # install pip reqs - cmd = f'pip3 install {item}' - cmd_array = shlex.split(cmd) - try: - if not DEBUG: - cp = subprocess.run(cmd_array) - cp.check_returncode() - else: - print(f'DEBUG: {cmd}') - except Exception as error: - print(f'Could not install {item}: {error}') - exit() - - # -------------------------------------------------------------------------- - # Make any necessary directories - # -------------------------------------------------------------------------- - def do_dirs(self): - - # check for empty/no list - if not (self.__needs_step('dirs')): - return - - # show some text - print('Creating directories') - - # for each folder we need to make - for item in self.conf_dict['dirs']: - - # show that we are doing something - print(f'Creating directory {item}') - - # make the folder(s) - try: - if not DEBUG: - os.makedirs(item, exist_ok=True) - else: - print(f'DEBUG: {item}') - except Exception as error: - print(f'Could not create directory {item}: {error}') - exit() - - # -------------------------------------------------------------------------- - # Copy all files to their dests - # -------------------------------------------------------------------------- - def do_files(self): - - # check for empty/no list - if not (self.__needs_step('files')): - return - - # show some text - print('Copying files') - - # for each file we need to copy - for key, val in self.conf_dict['files'].items(): - - # show that we are doing something - print(f'Copying {key} to {val}') - - # convert relative path to absolute path - abs_key = os.path.join(self.src_dir, key) - - # copy the file - try: - if not DEBUG: - shutil.copy(abs_key, val) - else: - print(f'DEBUG: {abs_key} : {val}') - except Exception as error: - print(f'Could not copy file {abs_key}: {error}') - exit() - - # -------------------------------------------------------------------------- - # Run postflight scripts - # -------------------------------------------------------------------------- - def do_postflight(self): - - # check for empty/no list - if not (self.__needs_step('postflight')): - return - - # show some text - print('Running postflight scripts') - - for item in self.conf_dict['postflight']: - - # show that we are doing something - print(f'Running {item}') - - # make relative file into absolute - abs_item = os.path.join(self.src_dir, item) - - # run item - cmd = abs_item - cmd_array = shlex.split(cmd) - try: - if not DEBUG: - subprocess.run(cmd_array) - else: - print(f'DEBUG: {cmd}') - except Exception as error: - print(f'Could not run {cmd}: {error}') - exit() - - # -------------------------------------------------------------------------- - # Helpers - # -------------------------------------------------------------------------- - - # -------------------------------------------------------------------------- - # Load dictionary data from a file - # -------------------------------------------------------------------------- - def __load_conf(self): - - # make sure the file exists - if not os.path.exists(self.conf_path): - print('Could not find config file') - exit() - - # read config file - with open(self.conf_path, 'r') as file: - try: - self.conf_dict = json.load(file) - - # make sure we have minimum keys - self.__merge_conf_dicts() - except Exception as error: - print(f'could not load config file: {error}') - exit() - - # -------------------------------------------------------------------------- - # Perform substitutions for paths in config file - # -------------------------------------------------------------------------- - def __make_subs(self): - - # get current user's home dir - home_dir = os.path.expanduser('~') - - # the dict of substitutions - sub_dict = { - '${HOME}': home_dir - } - - # create a temporary dict (can't modify conf_dict while iterating) - tmp_dict = self.conf_dict.copy() - - # for each section in the conf dict - for sect_name, sect_dict in tmp_dict.items(): - - # if it's a list - if isinstance(sect_dict, list): - - # empty the target list - self.conf_dict[sect_name] = [] - - # for each item - for item in sect_dict: - - # assume unchanged - tmp_item = item - - # if it's a string - if isinstance(item, str): - - # do the substitution - for sub_key, sub_val in sub_dict.items(): - tmp_item = tmp_item.replace(sub_key, sub_val) - - # modify user list - self.conf_dict[sect_name].append(tmp_item) - - # if it's a dict - elif isinstance(sect_dict, dict): - - # for each kv pair in dict - for key, val in sect_dict.items(): - - # assume unchanged - tmp_val = val - - # if the val is a string - if isinstance(val, str): - - # do the substitution - for sub_key, sub_val in sub_dict.items(): - tmp_val = tmp_val.replace(sub_key, sub_val) - - # modify user dict - self.conf_dict[sect_name][key] = tmp_val - - # -------------------------------------------------------------------------- - # Check if we are going to need sudo password and get it now - # -------------------------------------------------------------------------- - def __check_sudo(self): - - # if either of theses steps is required, we need sudo - if self.__needs_step('sys_reqs') or self.__needs_step('py_reqs'): - - # ask for sudo password now - cmd = 'sudo echo -n' - cmd_array = shlex.split(cmd) - subprocess.run(cmd_array) - - # -------------------------------------------------------------------------- - # Check if a step needs to be performed or can be skipped - # -------------------------------------------------------------------------- - def __needs_step(self, step): - - # if the section is present - if step in self.conf_dict.keys(): - conf_dict = self.conf_dict[step] - - # if there are entries in the section - if len(conf_dict): - return True - - # otherwise we can skip this step - return False - - def __merge_conf_dicts(self): - - # NB: there is probably a better way to do this - # this is mainly to make sure no one futzed with the config - # file manually and deleted or mistyped a key - # also note we don't do any value type checking (i.e. string or - # int) and no value clamping/validation - # basically, DON'T EDIT THE FILE BY HAND!!! - - # set defaults for any missing sections - - # get two dicts (src and dst) - dict_def = self.conf_dict_def - dict_user = self.conf_dict - - # iterate over src, adding any missing keys to dst - for key in dict_def.keys(): - if key not in dict_user.keys(): - dict_user[key] = dict_def[key] - - # do second-level kv defaults - for key in dict_def.keys(): - - # get two dicts (src and dst) - dict_def_2 = dict_def[key] - dict_user_2 = dict_user[key] - - if isinstance(dict_def_2, dict): - # iterate over src, adding any missing keys to dst - for key in dict_def_2.keys(): - if key not in dict_user_2.keys(): - dict_user_2[key] = dict_def_2[key] - elif isinstance(dict_def_2, list): - for item in dict_def_2: - if item not in dict_user_2: - dict_user_2.append(item) - -# def __run(self, cmd=''): -# try: -# cp = subprocess.run(shlex.split(cmd), check=True) -# except CalledProcessError as error: -# pass - - -# ------------------------------------------------------------------------------ -# Run the main class if we are not an import -# ------------------------------------------------------------------------------ -if __name__ == '__main__': - installer = Installer() - installer.run() - -# -) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1a256ed --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,42 @@ +# ------------------------------------------------------------------------------ +# Filename: pyproject.toml / \ +# Project : Installerator | () | +# Date : 10/28/2022 | | +# Author : cyclopticnerve | \____/ | +# License : WTFPLv2 \ / +# ------------------------------------------------------------------------------ + +[build-system] +requires = [ + "setuptools>=59.6.0", + "wheel" +] +build-backend = "setuptools.build_meta" + +[project] +name = "installerator" # package/module name, small letters +version = "0.1.0" +description = "A small Python module that makes installing Python apps easier" +readme = "README.md" +authors = [ + {name = "cyclopticnerve", email = "cyclopticnerve@gmail.com"} +] +license = {file = "LICENSE"} +classifiers = [ + "License :: Other/Proprietary License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", +] +keywords = [ + "install", + "installer", + "installation" +] +dependencies = ["configurator"] +requires-python = ">=3.10" + +[project.urls] +Homepage = "https://github.com/cyclopticnerve/Installerator" + +# -) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..406a97c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +build==0.9.0 +mccabe==0.7.0 +packaging==21.3 +pep517==0.13.0 +pycodestyle==2.9.1 +pydocstyle==6.1.1 +pyflakes==2.5.0 +pylama==8.4.1 +pyparsing==3.0.9 +snowballstemmer==2.2.0 +tomli==2.0.1 diff --git a/src/installerator/__init__.py b/src/installerator/__init__.py new file mode 100644 index 0000000..d2abc4c --- /dev/null +++ b/src/installerator/__init__.py @@ -0,0 +1,15 @@ +# ------------------------------------------------------------------------------ +# Filename: __init__.py / \ +# Project : Installerator | () | +# Date : 10/03/2022 | | +# Author : cyclopticnerve | \____/ | +# License : WTFPLv2 \ / +# ------------------------------------------------------------------------------ + +__version__ = "0.1.0" + +''' +A small Python module that makes installing Python apps easier +''' + +# -) diff --git a/src/installerator/base_installerator.py b/src/installerator/base_installerator.py new file mode 100644 index 0000000..98a7d60 --- /dev/null +++ b/src/installerator/base_installerator.py @@ -0,0 +1,209 @@ +# -----------------------------------------------------------------------------# +# Filename: base_installerator.py / \ # +# Project : Installerator | () | # +# Date : 10/03/2022 | | # +# Author : cyclopticnerve | \____/ | # +# License : WTFPLv2 \ / # +# -----------------------------------------------------------------------------# + +# ------------------------------------------------------------------------------ +# Imports +# ------------------------------------------------------------------------------ + +# global imports +import os +import shlex +import subprocess + +# local imports +import configurator + +# ------------------------------------------------------------------------------ +# Constants +# ------------------------------------------------------------------------------ + +DEBUG = 0 + + +# ------------------------------------------------------------------------------ +# Define the main class +# ------------------------------------------------------------------------------ + +class Base_Installerator: + + # -------------------------------------------------------------------------- + # Public methods + # -------------------------------------------------------------------------- + + # -------------------------------------------------------------------------- + # Initialize the class + # -------------------------------------------------------------------------- + def __init__(self): + + ''' + The default initialization of the class + ''' + + # do nothing + pass + + # -------------------------------------------------------------------------- + # Private methods + # -------------------------------------------------------------------------- + + # -------------------------------------------------------------------------- + # Run the script + # -------------------------------------------------------------------------- + def _run(self, dict_user): + + ''' + Runs the setup using the supplied user dictionary + + Paramaters: + dict_user [dict]: the user dict to get options from + ''' + + # the defs dict + dict_defs = { + 'general': { + 'name': '' + }, + 'preflight': [ + ], + 'sys_reqs': [ + ], + 'py_reqs': [ + ], + 'dirs': [ + ], + 'files': { + }, + 'postflight': [ + ] + } + + # get current user's home dir + home_dir = os.path.expanduser('~') + + # get location + src_dir = os.path.dirname(os.path.abspath(__file__)) + + # the default dict of substitutions + dict_subs = { + '${HOME}': home_dir, + '${SRC}': src_dir + } + + # do the config merge + self.dict_conf = configurator.load(dict_defs, dict_user, dict_subs) + + # -------------------------------------------------------------------------- + # Check if we are going to need sudo password and get it now + # -------------------------------------------------------------------------- + def _check_sudo(self): + + ''' + Checks if we need sudo permission early in the install + + Returns: + ret [bool]: True if we need sudo permission, False if we don't + ''' + + # if either of theses steps is required, we need sudo + if self._needs_step('sys_reqs') or self._needs_step('py_reqs'): + + # ask for sudo password now + cmd = 'sudo echo -n' + cmd_array = shlex.split(cmd) + subprocess.run(cmd_array) + + # -------------------------------------------------------------------------- + # Run preflight scripts + # -------------------------------------------------------------------------- + def _do_preflight(self): + + ''' + Run preflight scripts (before we do the heavy lifting) + ''' + + # run preflight scripts + self._run_scripts('preflight') + + # -------------------------------------------------------------------------- + # Run postflight scripts + # -------------------------------------------------------------------------- + def _do_postflight(self): + + ''' + Run postflight scripts (after we do the heavy lifting) + ''' + + # run postflight scripts + self._run_scripts('postflight') + + # -------------------------------------------------------------------------- + # Check if a step needs to be performed or can be skipped + # -------------------------------------------------------------------------- + def _needs_step(self, step): + + ''' + Check if an entry in the defs/user needs to be run + + Paramaters: + step [str]: the step to check for in the final dict + + Returns: + ret [bool]:True if the dict contains the step, False otherwise + ''' + + # if the section is present + if step in self.dict_conf.keys(): + + # if there are entries in the section + dict_conf = self.dict_conf[step] + if len(dict_conf): + return True + + # otherwise we can skip this step + return False + + # -------------------------------------------------------------------------- + # Run preflight/postflight scripts + # -------------------------------------------------------------------------- + def _run_scripts(self, step): + + ''' + Runs the scripts from preflight or postflight + + Paramaters: + step [str]: The step to run, either preflight or postflight + + Returns: + ret [int]: Not Implemented yet + ''' + + # check for empty/no list + if not self._needs_step(step): + return + + # show some text + print(f'Running {step} scripts') + + for item in self.dict_conf[step]: + + # show that we are doing something + print(f'Running {item}... ', end='') + + # run item + cmd_array = shlex.split(item) + try: + if not DEBUG: + cp = subprocess.run(cmd_array, check=True) + cp.check_returncode() + print('Done') + except Exception as error: + print('Fail') + print(f'Could not run {item}: {error}') + exit() + +# -) diff --git a/src/installerator/installerator.py b/src/installerator/installerator.py new file mode 100755 index 0000000..aec0303 --- /dev/null +++ b/src/installerator/installerator.py @@ -0,0 +1,229 @@ +# -----------------------------------------------------------------------------# +# Filename: installerator.py / \ # +# Project : Installerator | () | # +# Date : 09/29/2022 | | # +# Author : cyclopticnerve | \____/ | # +# License : WTFPLv2 \ / # +# -----------------------------------------------------------------------------# + +# ------------------------------------------------------------------------------ +# Imports +# ------------------------------------------------------------------------------ + +# global imports +import os +import shlex +import shutil +import subprocess + +# local imports +from installerator.base_installerator import Base_Installerator + +# ------------------------------------------------------------------------------ +# Constants +# ------------------------------------------------------------------------------ + +DEBUG = 0 + + +# ------------------------------------------------------------------------------ +# Define the main class +# ------------------------------------------------------------------------------ + +class Installerator(Base_Installerator): + + # -------------------------------------------------------------------------- + # Public methods + # -------------------------------------------------------------------------- + + # -------------------------------------------------------------------------- + # Initialize the class + # -------------------------------------------------------------------------- + def __init__(self): + + ''' + The default initialization of the class + ''' + + # base installer init + super().__init__() + + # -------------------------------------------------------------------------- + # Run the script + # -------------------------------------------------------------------------- + def run(self, dict_user): + + ''' + Runs the setup using the supplied user dictionary + + Paramaters: + dict_user [dict]: the user dict to get options from + ''' + + # base installer run + super()._run(dict_user) + + # check if we need sudo password + super()._check_sudo() + + # show some text + prog_name = self.dict_conf['general']['name'] + print(f'Installing {prog_name}') + + super()._do_preflight() + self._do_sys_reqs() + self._do_py_reqs() + self._do_dirs() + self._do_files() + super()._do_postflight() + + # done installing + print(f'{prog_name} installed') + + # -------------------------------------------------------------------------- + # Private methods + # -------------------------------------------------------------------------- + + # -------------------------------------------------------------------------- + # Install system prerequisites + # -------------------------------------------------------------------------- + def _do_sys_reqs(self): + + ''' + Install any system requirements + ''' + + # check for pip necessary + if super()._needs_step('py-reqs'): + self.dict_conf['sys_reqs'].append('python3-pip') + + # check for empty/no list + if not super()._needs_step('sys_reqs'): + return + + # show some text + print('Installing system requirements') + + # get system requirements + for item in self.dict_conf['sys_reqs']: + + # show that we are doing something + print(f'Installing {item}... ', end='') + + # install apt reqs + cmd = f'sudo apt-get install {item} -qq > /dev/null' + cmd_array = shlex.split(cmd) + try: + if not DEBUG: + cp = subprocess.run(cmd_array) + cp.check_returncode() + print('Done') + except Exception as error: + print('Fail') + print(f'Could not install {item}: {error}') + exit() + + # -------------------------------------------------------------------------- + # Install python prerequisites + # -------------------------------------------------------------------------- + def _do_py_reqs(self): + + ''' + Install any Python requirements + ''' + + # check for empty/no list + if not super()._needs_step('py_reqs'): + return + + # show some text + print('Installing python requirements') + + # get python requirements + for item in self.dict_conf['py_reqs']: + + # show that we are doing something + print(f'Installing {item}... ', end='') + + # install pip reqs + cmd = f'pip3 install -q {item} > /dev/null' + cmd_array = shlex.split(cmd) + try: + if not DEBUG: + cp = subprocess.run(cmd_array) + cp.check_returncode() + print('Done') + except Exception as error: + print('Fail') + print(f'Could not install {item}: {error}') + exit() + + # -------------------------------------------------------------------------- + # Make any necessary directories + # -------------------------------------------------------------------------- + def _do_dirs(self): + + ''' + Create any required folders + ''' + + # check for empty/no list + if not super()._needs_step('dirs'): + return + + # show some text + print('Creating directories') + + # for each folder we need to make + for item in self.dict_conf['dirs']: + + # show that we are doing something + print(f'Creating directory {item}... ', end='') + + # make the folder(s) + try: + if not DEBUG: + os.makedirs(item, exist_ok=True) + print('Done') + except Exception as error: + print('Fail') + print(f'Could not create directory {item}: {error}') + exit() + + # -------------------------------------------------------------------------- + # Copy all files to their dests + # -------------------------------------------------------------------------- + def _do_files(self): + + ''' + Copy (or create) any required files + ''' + + # check for empty/no list + if not super()._needs_step('files'): + return + + # show some text + print('Copying files') + + # for each file we need to copy + for src, dst in self.dict_conf['files'].items(): + + # show that we are doing something + print(f'Copying {src} to {dst}... ', end='') + + # NB: removed because all paths should be absolute + # convert relative path to absolute path + # abs_src = os.path.join(self.src_dir, src) + + # copy the file + try: + if not DEBUG: + shutil.copy(src, dst) + print('Done') + except Exception as error: + print('Fail') + print(f'Could not copy file {src}: {error}') + exit() + +# -) diff --git a/src/installerator/uninstallerator.py b/src/installerator/uninstallerator.py new file mode 100755 index 0000000..fc02634 --- /dev/null +++ b/src/installerator/uninstallerator.py @@ -0,0 +1,149 @@ +# -----------------------------------------------------------------------------# +# Filename: uninstallerator.py / \ # +# Project : Installerator | () | # +# Date : 09/29/2022 | | # +# Author : cyclopticnerve | \____/ | # +# License : WTFPLv2 \ / # +# -----------------------------------------------------------------------------# + +# ------------------------------------------------------------------------------ +# Imports +# ------------------------------------------------------------------------------ + +# global imports +import os +import shutil + +# local imports +from installerator.base_installerator import Base_Installerator + +# ------------------------------------------------------------------------------ +# Constants +# ------------------------------------------------------------------------------ + +DEBUG = 0 + + +# ------------------------------------------------------------------------------ +# Define the main class +# ------------------------------------------------------------------------------ + +class Uninstallerator(Base_Installerator): + + # -------------------------------------------------------------------------- + # Public methods + # -------------------------------------------------------------------------- + + # -------------------------------------------------------------------------- + # Initialize the class + # -------------------------------------------------------------------------- + def __init__(self): + + ''' + The default initialization of the class + ''' + + # base installer init + super().__init__() + + # -------------------------------------------------------------------------- + # Run the script + # -------------------------------------------------------------------------- + def run(self, dict_user): + + ''' + Runs the setup using the supplied user dictionary + + Paramaters: + dict_user [dict]: the user dict to get options from + ''' + + # base installer run + super()._run(dict_user) + + # show some text + prog_name = self.dict_conf['general']['name'] + print(f'Uninstalling {prog_name}') + + super()._do_preflight() + self._do_dirs() + self._do_files() + super()._do_postflight() + + # done uninstalling + print(f'{prog_name} uninstalled') + + # -------------------------------------------------------------------------- + # Private methods + # -------------------------------------------------------------------------- + + # -------------------------------------------------------------------------- + # Delete any unnecessary directories + # -------------------------------------------------------------------------- + def _do_dirs(self): + + ''' + Delete any specified directories + ''' + + # check for empty/no list + if not super()._needs_step('dirs'): + return + + # show some text + print('Deleting directories') + + # for each folder we need to delete + for item in self.dict_conf['dirs']: + + # show that we are doing something + print(f'Deleting directory {item}... ', end='') + + # delete the folder + try: + if not DEBUG: + shutil.rmtree(item) + print('Done') + except Exception as error: + print('Fail') + print(f'Could not delete directory {item}: {error}') + exit() + + # -------------------------------------------------------------------------- + # Delete any necessary files (outside above directiories) + # -------------------------------------------------------------------------- + def _do_files(self): + + ''' + Delete any specified files + ''' + + # check for empty/no list + if not super()._needs_step('files'): + return + + # show some text + print('Deleting files') + + # for each file we need to delete + for src, dst in self.dict_conf['files'].items(): + + # show that we are doing something + print(f'Deleting file {src}... ', end='') + + # NB: removed because all paths should be absolute + # convert relative path to absolute path + # abs_src = os.path.join(dst, src) + + # delete the file (if it'wasn't in a folder above) + if os.path.exists(src): + try: + if not DEBUG: + os.remove(src) + print('Done') + except Exception as error: + print('Fail') + print(f'Could not delete file {src}: {error}') + exit() + +# -) diff --git a/tests/install.py b/tests/install.py new file mode 100644 index 0000000..27bad7e --- /dev/null +++ b/tests/install.py @@ -0,0 +1,50 @@ + +# global imports +# import os +# import sys + +# curr_dir = os.path.abspath(os.path.dirname(__file__)) +# src = os.path.abspath(os.path.join(curr_dir, '../src')) +# sys.path.insert(1, src) + +# local imports +# from package.module import class +from installerator.installerator import Installerator # noqa E402 (ignore import order) + +# the user dict +dict_user = { + "general": { + "name": "SpaceOddity" + }, + "py_reqs": [ + "python-crontab" + ], + "dirs": [ + "${HOME}/.spaceoddity", + "${HOME}/.config/spaceoddity" + ], + "files": { + "${SRC}/spaceoddity.py": "${HOME}/.spaceoddity", + "${SRC}/LICENSE": "${HOME}/.spaceoddity", + "${SRC}/VERSION": "${HOME}/.spaceoddity", + "${SRC}/uninstall.py": "${HOME}/.spaceoddity", + "${SRC}/uninstall.json": "${HOME}/.spaceoddity", + "${SRC}/cron_uninstall.py": "${HOME}/.spaceoddity" + }, + "postflight": [ + "${SRC}/convert_json.py", + "${SRC}/cron_install.py", + "${HOME}/.spaceoddity/spaceoddity.py" + ] +} + +# ------------------------------------------------------------------------------ +# Run the main class if we are not an import +# ------------------------------------------------------------------------------ +if __name__ == '__main__': + + # create an instance of the class + inst = Installerator() + + # # run the instance + inst.run(dict_user) diff --git a/tests/uninstall.py b/tests/uninstall.py new file mode 100644 index 0000000..564aabf --- /dev/null +++ b/tests/uninstall.py @@ -0,0 +1,34 @@ + +# global imports +# import os +# import sys + +# curr_dir = os.path.abspath(os.path.dirname(__file__)) +# src = os.path.abspath(os.path.join(curr_dir, '../src')) +# sys.path.insert(1, src) + +# local imports +from installerator.uninstallerator import Uninstallerator # noqa E402 (ignore import order) + +dict_user = { + "general": { + "name": "SpaceOddity" + }, + "preflight": [ + "${HOME}/.spaceoddity/cron_uninstall.py" + ], + "dirs": [ + "${HOME}/.spaceoddity", + "${HOME}/.config/spaceoddity" + ] +} +# ------------------------------------------------------------------------------ +# Run the main class if we are not an import +# ------------------------------------------------------------------------------ +if __name__ == '__main__': + + # create an instance of the class + uninst = Uninstallerator() + + # # run the instance + uninst.run(dict_user) diff --git a/todo/checklist.txt b/todo/checklist.txt new file mode 100644 index 0000000..6f821d4 --- /dev/null +++ b/todo/checklist.txt @@ -0,0 +1,52 @@ +Project setup + +Nautilus + [X] Copy Template as new project folder + +VSCodium + [X] Create venv - $ python -m venv venv; $ source venv/bin/activate + [X] Install pylama in venv (popup in cottom right) + [X] Install build - $ python -m pip install build + [X] Create new repo in github tab + [X] Publish "main" branch + [X] Stage all changes + [X] Commit “first commit” + [X] Checkout new branch “dev” from “main” + [X] Sync changes + +GitHub + [X] Make “dev” default branch + [X] Add description and search tags + +-------------------------------------------------------------------------------- + +Work + - Do all work on dev + - Unit tests + [X] Internal test (sys.path) + [X] Install/test in venv (this project) + [X] Install/test in venv (test project) + [X] Headers/footers + [X] Docstrings + +-------------------------------------------------------------------------------- + +Release - VSCodium + [X] $ python -m pip freeze > requirements.txt + [X] remove '-e' from requirements + [X] Change version number in __init__.py and pyproject.toml + [X] Build - $ python -m build + [X] Push "dev" + [X] Checkout new branch “release” from “dev” + [X] Publish branch + -- Modify files as necessary (delete .gitignore, etc.) + [X] Change any DEBUG to 0 + [] Merge from “release” to “main“ (git checkout “main”; git merge “release”) + [] Delete release branch + [] Push “main” + [] DO NOT TAG HERE - TAG IN GITHUB (TAG CONFLICT?) + +Release - GitHub + [] Draft release + [] Tag with version number (v0.1.0) + [] Make sure target is main diff --git a/todo/howto.txt b/todo/howto.txt new file mode 100644 index 0000000..3d25d2e --- /dev/null +++ b/todo/howto.txt @@ -0,0 +1,80 @@ + +INITIAL + +project_name/ + src/ + package_name/ + __init__.py + module_name.py + tests/ + test.py + todo/ + checklist.txt + howto.txt + todo.txt + venv/ + .gitignore + LICENSE + MANIFEST.in + pyproject.toml + README.md + requiremwents.txt + +create venv +$ python -m venv venv + +!!! DON'T CHAGE NAME OF PROJECT FOLDER! IT WILL BREAK VENV!!! + +ctrl+shift+p - python:select interpreter +browse to venv/bin/python +install linter from popup in lower right + +-------------------------------------------------------------------------------- + +WORK + +docstrings +unit tests/pytest + +install from source in venv (this project) +$ (venv) python -m pip install -e . + +install from source in venv (other project) +$ (venv) ppython -m pip install -e /path/to/project/folder + +------------------------------------------------------------ + +RELEASE + +version number + +to build: +python -m build + +upload to testpypi/pypi +$ twine upload -r testpypi dist/* +$ twine upload dist/* + + +NEXT: install from testypi/pypi +$ python3 -m pip install -i https://test.pypi.org/simple/ project_name +$ python3 -m pip install project_name + +NEXT: install from github +https://packaging.python.org/en/latest/tutorials/installing-packages/#id23 +python3 -m pip git+https://github.com/cyclopticnerve/project_name/ +requirements? pip install -r requirements.txt + +python3 -m pip install -e git+https://github.com/cyclopticnerve/.git#egg= # from default branch +python3 -m pip install -e git+https://github.com/cyclopticnerve/.git@maine#egg= # from specific branch + + + + + +NEXT: Meta: +Project name (capitalized) +Version number +Copyright +Short desc (for git and setup) +Tags (for git and setup) \ No newline at end of file diff --git a/todo/run_as_root.txt b/todo/run_as_root.txt new file mode 100644 index 0000000..e23edc1 --- /dev/null +++ b/todo/run_as_root.txt @@ -0,0 +1,18 @@ +'general': { + 'run_as_root': False +} + +# check for run as root/need to run as root +run_as_root = self.conf_dict['general']['run_as_root'] +file_name = os.path.basename(__file__) +run_root = (os.geteuid() == 0) +if run_as_root and not run_root: + msg = 'This script needs to be run as root. '\ + f'Try \'sudo ./{file_name}\'' + print(msg) + exit() +elif not run_as_root and run_root: + msg = 'This script should not be run as root. '\ + f'Try \'./{file_name}\'' + print(msg) + exit() diff --git a/todo/todo.txt b/todo/todo.txt new file mode 100644 index 0000000..cc1b6b9 --- /dev/null +++ b/todo/todo.txt @@ -0,0 +1,7 @@ +NEXT: pre/postflight exit codes +NEXT: cmdline option for path to config file +NEXT: add version string to options and print at start of install +NEXT: custom substitutions? +NEXT: all possible entries in pyproject.toml +NEXT: can we download current in one shot? python -m pip install git+https://... +And can we do that with releses? download from main or latest? diff --git a/uninstall.py b/uninstall.py deleted file mode 100755 index 53c4132..0000000 --- a/uninstall.py +++ /dev/null @@ -1,243 +0,0 @@ -#!/usr/bin/env python3 -# -----------------------------------------------------------------------------# -# Filename: uninstall.py / \ # -# Project : Installer | () | # -# Date : 09/29/2022 | | # -# Author : Dana Hynes | \____/ | # -# License : WTFPLv2 \ / # -# -----------------------------------------------------------------------------# - -# NEXT: less output, only print step name and ... Done -# NEXT: pre/postflight exit codes -# NEXT: load_conf should load vars from json file - -# ------------------------------------------------------------------------------ -# Imports -# ------------------------------------------------------------------------------ - -import os -import shlex -import shutil -import subprocess - -# ------------------------------------------------------------------------------ -# Define the main class -# ------------------------------------------------------------------------------ - - -class Uninstaller: - - # -------------------------------------------------------------------------- - # Methods - # -------------------------------------------------------------------------- - - # -------------------------------------------------------------------------- - # Initialize the class - # -------------------------------------------------------------------------- - def __init__(self): - - # get current user's home dir - self.home_dir = os.path.expanduser('~') - - # get current dir - self.src_dir = os.path.dirname(os.path.abspath(__file__)) - - # these are the values to set in preflight - self.run_as_root = False - self.prog_name = '' - self.disp_name = '' - - self.preflight = [] - self.dirs = [] - self.files = {} - self.postflight = [] - - # -------------------------------------------------------------------------- - # Run the script - # -------------------------------------------------------------------------- - def run(self): - - # set options - self.__load_conf() - - # check for run as root/need to run as root - file_name = os.path.basename(__file__) - run_root = (os.geteuid() == 0) - if self.run_as_root and not run_root: - msg = 'This script needs to be run as root. '\ - f'Try \'sudo ./{file_name}\'' - print(msg) - exit() - elif not self.run_as_root and run_root: - msg = 'This script should not be run as root. '\ - f'Try \'./{file_name}\'' - print(msg) - exit() - - # show some text - print(f'Uninstalling {self.disp_name}') - - # do the steps in order - self.do_preflight() - self.do_dirs() - self.do_files() - self.do_postflight() - - # done uninstalling - print(f'{self.disp_name} uninstalled') - - # -------------------------------------------------------------------------- - # Steps - # -------------------------------------------------------------------------- - - # -------------------------------------------------------------------------- - # Run preflight scripts - # -------------------------------------------------------------------------- - - def do_preflight(self): - - # show some text - print('Running preflight scripts') - - for item in self.preflight: - - # show that we are doing something - print(f'Running {item}') - - # make relative file into absolute - abs_item = os.path.join(self.src_dir, item) - - # run item - cmd = abs_item - cmd_array = shlex.split(cmd) - try: - subprocess.run(cmd_array) - except Exception as error: - print(f'Could not run {cmd}:', error) - exit() - - # -------------------------------------------------------------------------- - # Delete any necessary directories - # -------------------------------------------------------------------------- - def do_dirs(self): - - # show some text - print('Deleting directories') - - # for each folder we need to delete - for item in self.dirs: - - # show that we are doing something - print(f'Deleting directory {item}') - - # delete the folder - try: - shutil.rmtree(item) - except Exception as error: - - # not a fatal error - print(f'Could not delete directory {item}:', error) - - # -------------------------------------------------------------------------- - # Delete any necessary files (outside above directiories) - # -------------------------------------------------------------------------- - def do_files(self): - - # show some text - print('Deleting files') - - # for each file we need to delete - for key, val in self.files.items(): - - # convert relative path to absolute path - abs_key = os.path.join(self.src_dir, key) - - # show that we are doing something - print(f'Deleting file {abs_key}') - - # delete the file (if it'wasn't in a folder above) - if os.path.exists(abs_key): - try: - os.remove(abs_key) - except Exception as error: - print(f'Could not delete file {abs_key}:', error) - - # -------------------------------------------------------------------------- - # Run postflight scripts - # -------------------------------------------------------------------------- - def do_postflight(self): - - # show some text - print('Running postflight scripts') - - for item in self.postflight: - - # show that we are doing something - print(f'Running {item}') - - # make relative file into absolute - abs_item = os.path.join(self.src_dir, item) - - # run item - cmd = abs_item - cmd_array = shlex.split(cmd) - try: - subprocess.run(cmd_array) - except Exception as error: - print(f'Could not run {cmd}:', error) - exit() - -# -------------------------------------------------------------------------- -# Helpers -# -------------------------------------------------------------------------- - - # -------------------------------------------------------------------------- - # Set options from __init__ - # -------------------------------------------------------------------------- - def __load_conf(self): - - # set root required - self.run_as_root = False # default - - # the program name - self.prog_name = 'spaceoddity' - self.disp_name = 'SpaceOddity' - - # preflight scripts - # NB: these should be relative to src_dir - self.preflight = [ - # default empty - ] - - # get some dirs - dst_dir = os.path.join(self.home_dir, f'.{self.prog_name}') - cfg_dir = os.path.join(self.home_dir, '.config', f'{self.prog_name}') - - # delete dirs - # NB: these should be absolute paths - self.dirs = [ - dst_dir, - cfg_dir - ] - - # delete files - # NB: key is relative to src_dir, value is absolute - self.files = { - # default empty - } - - # postflight scripts - # NB: these should be relative to src_dir - self.postflight = [ - 'cron_uninstall.py' - ] - - -# ------------------------------------------------------------------------------ -# Run the main class if we are not an import -# ------------------------------------------------------------------------------ -if __name__ == '__main__': - uninstaller = Uninstaller() - uninstaller.run() - -# -)