diff --git a/.gitignore b/.gitignore index 226ccf4cd..0c4fae104 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,6 @@ /tmp /vcpkg /vcpkg_installed -/scripts/**/*.pyc /.cache /.venv aqtinstall.log @@ -37,3 +36,6 @@ CMakeFiles/* # vscode folder /.vscode + +# scripts folder +/scripts diff --git a/scripts/lib/cmd_utils.py b/scripts/lib/cmd_utils.py deleted file mode 100644 index 26fbaa958..000000000 --- a/scripts/lib/cmd_utils.py +++ /dev/null @@ -1,156 +0,0 @@ -# Deskflow -- mouse and keyboard sharing utility -# Copyright (C) 2024 Symless Ltd. -# -# This package is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# found in the file LICENSE that should have accompanied this file. -# -# This package is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import subprocess -import sys -import lib.env as env - -try: - import colorama # type: ignore - from colorama import Fore # type: ignore - - colorama.init() -except ImportError: - - class Fore: - RESET = "" - YELLOW = "" - - -def has_command(command): - platform = sys.platform - if platform == "win32": - cmd = f"where {command}" - else: - cmd = f"which {command}" - try: - subprocess.check_output(cmd, shell=True) - return True - except subprocess.CalledProcessError: - return False - - -def strip_continuation_sequences(command, strip_newlines=True): - """ - Remove the continuation sequences (\\) from a command. - - To spread strings over multiple lines in YAML files, like in bash, a backslash is used at - the end of each line as continuation character. - """ - - if isinstance(command, list): - raise ValueError("List commands are not supported") - - cmd_continuation = " \\" - command = command.replace(cmd_continuation, "") - - # Some versions of pyyaml will remove the newlines already, so always stripping - # makes the output more consistent. - if strip_newlines: - command = command.replace("\n", " ") - - return command - - -def run( - command, - check=True, # true by default to fail fast - shell=False, # false by default for security - get_output=False, - print_cmd=False, # false by default for security -): - """ - Convenience wrapper around `subprocess.run` to: - - print the command before running it (if `print_cmd` is True) - - This differs to `subprocess.run` in that by default it: - - checks the return code by default - - prints list commands as a readable string on failure - - This is the same as `subprocess.run` in that it: - - does not use shell by default for security (shell is less secure) - - Args: - command (str or list): The command to run. - check (bool): Raise an exception if the command fails. - shell (bool): Run the command in a shell (false by default for security) - get_output (bool): Return the output of the command. - print_cmd (bool): Print the command before running it (false by default for security) - """ - - is_list_cmd = isinstance(command, list) - - # create string version of list command, only for debugging purposes - command_str = command - if is_list_cmd: - command_str = " ".join(command) - - if print_cmd: - print(f"Running: {command_str}") - else: - print("Running command...") - command_str = "***" - - # TODO: You can definitely use a list command with shell=True on Windows, - # but can you use a string command with shell=False on Windows? - # - # The `subprocess.run` function has a little gotcha: - # - a string command must be used when `shell=True` - # - a list command must be used when shell isn't or `shell=False` - # however, it allows you to pass a string command when shell isn't used or `shell=False` - # then fails with a vague error message. same problem with list commands and `shell=True` - if not env.is_windows() and is_list_cmd and shell: - raise ValueError("List commands cannot be used when shell=True on Unix systems") - elif not is_list_cmd and not shell: - raise ValueError("String commands cannot be used when shell=False or not set") - - # Flush the output to ensure the command is printed before the output of the command, - # which seems to happen in the GitHub runner logs. - sys.stdout.flush() - sys.stderr.flush() - - try: - if get_output: - result = subprocess.run( - command, - shell=shell, - check=check, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - else: - result = subprocess.run(command, check=check, shell=shell) - - except subprocess.CalledProcessError as e: - # Take control of how failed commands are printed: - # - if `print_cmd` is false, it will print `***` instead of the command - # - if the command was a list, the command is printed as a readable string - raise RuntimeError( - f"Command exited with code {e.returncode}: {command_str}" - ) from None - except Exception: - # Take control of how failed commands are printed: - # - if `print_cmd` is false, it will print `***` instead of the command - # - if the command was a list, the command is printed as a readable string - raise RuntimeError(f"Command failed: {command_str}") - - if result.returncode != 0: - print( - f"{Fore.YELLOW}Command exited with code {result.returncode}:{Fore.RESET} {command_str}", - file=sys.stderr, - ) - - return result diff --git a/scripts/lib/colors.py b/scripts/lib/colors.py deleted file mode 100644 index ff020aa44..000000000 --- a/scripts/lib/colors.py +++ /dev/null @@ -1,24 +0,0 @@ -# Deskflow -- mouse and keyboard sharing utility -# Copyright (C) 2024 Symless Ltd. -# -# This package is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# found in the file LICENSE that should have accompanied this file. -# -# This package is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import colorama # type: ignore -from colorama import Fore # type: ignore - -colorama.init() - -SUCCESS_TEXT = f"{Fore.LIGHTGREEN_EX}Success:{Fore.RESET}" -ERROR_TEXT = f"{Fore.LIGHTRED_EX}Error:{Fore.RESET}" -WARNING_TEXT = f"{Fore.LIGHTYELLOW_EX}Warning:{Fore.RESET}" -HINT_TEXT = f"{Fore.LIGHTBLUE_EX}Hint:{Fore.RESET}" diff --git a/scripts/lib/env.py b/scripts/lib/env.py deleted file mode 100644 index 464be46c7..000000000 --- a/scripts/lib/env.py +++ /dev/null @@ -1,269 +0,0 @@ -# Deskflow -- mouse and keyboard sharing utility -# Copyright (C) 2024 Symless Ltd. -# -# This package is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# found in the file LICENSE that should have accompanied this file. -# -# This package is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import os, sys, subprocess -import lib.cmd_utils as cmd_utils - -# The `.venv` dir seems to be most common for virtual environments. -VENV_DIR = ".venv" - - -def check_module(module): - try: - __import__(module) - return True - except ImportError: - print(f"Python is missing {module} module", file=sys.stderr) - return False - - -def get_os(): - """Detects the operating system.""" - if sys.platform == "win32": - return "windows" - elif sys.platform == "darwin": - return "mac" - elif sys.platform.startswith("linux"): - return "linux" - else: - raise RuntimeError(f"Unsupported platform: {sys.platform}") - - -def is_windows(): - return get_os() == "windows" - - -def is_mac(): - return get_os() == "mac" - - -def is_linux(): - return get_os() == "linux" - - -def get_linux_distro(): - """Detects the Linux distro.""" - os_file = "/etc/os-release" - name = None - name_like = None - version_id = None - version_codename = None - - if os.path.isfile(os_file): - with open(os_file) as f: - for line in f: - if line.startswith("ID="): - name = line.strip().split("=")[1].strip('"') - elif line.startswith("ID_LIKE="): - name_like = line.strip().split("=")[1].strip('"') - elif line.startswith("VERSION_ID="): - version_id = line.strip().split("=")[1].strip('"') - elif line.startswith("VERSION_CODENAME="): - version_codename = line.strip().split("=")[1].strip('"') - - return name, name_like, version_id or version_codename - - -def get_env(name, required=True, default=None): - """ - Returns an env var (stripped) or optionally raises an error if not set. - - If `default` is set, it will be returned even if `required` is True. - """ - value = os.getenv(name) - if value: - value = value.strip() - - if not value: - if default: - return default - elif required: - raise ValueError(f"Required env var not set: {name}") - - return value - - -def get_env_bool(name, default=False): - """Returns a boolean value from an env var (stripped).""" - value = os.getenv(name) - if value: - value = value.strip() - - if value is None: - return default - - return value.lower() in ["true", "1", "yes"] - - -def get_venv_executable(binary="python"): - if sys.platform == "win32": - return os.path.join(VENV_DIR, "Scripts", binary) - else: - return os.path.join(VENV_DIR, "bin", binary) - - -def in_venv(): - """Returns True if the script is running in a Python virtual environment.""" - return sys.prefix != sys.base_prefix - - -def ensure_in_venv(script_file, create_venv=False): - """ - Ensures the script is running in a Python virtual environment (venv). - If the script is not running in a venv, it will create one and re-run the script in the venv. - """ - - check_dependencies(raise_error=True) - import venv - - if in_venv(): - print(f"Running in venv, executable: {sys.executable}", flush=True) - return - - if create_venv and not os.path.exists(VENV_DIR): - print(f"Creating virtual environment at {VENV_DIR}") - venv.create(VENV_DIR, with_pip=True) - - if os.path.exists(VENV_DIR): - script_file_abs = os.path.abspath(script_file) - print(f"Using virtual environment for: {script_file_abs}", flush=True) - python_executable = get_venv_executable() - result = subprocess.run([python_executable, script_file_abs] + sys.argv[1:]) - sys.exit(result.returncode) - else: - print( - "The Python virtual environment (.venv) needs to be created before you can " - "run this script.\n" - "Please run: scripts/setup_venv.py" - ) - sys.exit(1) - - -def install_requirements(): - """ - Uses `pip` to install required Python modules from the `requirements.txt` file. - """ - - check_dependencies(raise_error=True) - - print("Updating pip...") - cmd_utils.run( - [sys.executable, "-m", "pip", "install", "--upgrade", "pip"], - shell=False, - print_cmd=True, - ) - - print("Installing required modules...") - cmd_utils.run( - [sys.executable, "-m", "pip", "install", "-e", "scripts"], - shell=False, - print_cmd=True, - ) - - -def check_dependencies(raise_error=False): - """ - Returns True if pip and venv are available. - """ - - has_pip = check_module("pip") - has_venv = check_module("venv") - - if raise_error: - if not has_pip: - raise RuntimeError("Python is missing pip") - if not has_venv: - raise RuntimeError("Python is missing venv") - else: - return has_pip and has_venv - - -def ensure_dependencies(): - """ - Ensures that pip and venv are available, and installs them if they are not. - This is normally only installs on Linux, as Windows and Mac usually come with pip and venv. - """ - - if check_dependencies(): - return - - print("Installing Python dependencies...") - - os = get_os() - if os != "linux": - # should not be a problem, since windows and mac come with pip and venv - raise RuntimeError(f"Unable to install Python dependencies on {os}") - - has_sudo = cmd_utils.has_command("sudo") - sudo = "sudo" if has_sudo else "" - - distro, distro_like, _version = get_linux_distro() - if not distro_like: - distro_like = distro - - update_cmd = None - install_cmd = None - if distro == "rhel" or "rhel" in distro_like: - update_cmd = "yum check-update" - install_cmd = "yum install -y python3-pip" # rhel-like has venv already - elif "debian" in distro_like: - update_cmd = "apt update" - install_cmd = "apt install -y python3-pip python3-venv" - elif "fedora" in distro_like: - update_cmd = "dnf check-update" - install_cmd = "dnf install -y python3-pip python3-virtualenv" - elif "arch" in distro_like: - install_cmd = "pacman -Syu --noconfirm python-pip python-virtualenv" - elif "opensuse" in distro_like: - update_cmd = "zypper refresh" - install_cmd = "zypper install -y python3-pip python3-virtualenv" - else: - raise RuntimeError(f"Unable to install Python dependencies on {distro}") - - if update_cmd: - # don't check the return code, as some package managers return non-zero exit codes - # under normal circumstances (e.g. dnf check-update returns 100 when there are - # updates available). - cmd_utils.run( - f"{sudo} {update_cmd}".strip(), check=False, shell=True, print_cmd=True - ) - - cmd_utils.run(f"{sudo} {install_cmd}".strip(), shell=True, print_cmd=True) - - -def import_colors(): - import lib.colors as colors - - return colors - - -def persist_lock_file(path): - """ - Persists a lock file and ensures the directory part of the path exists. - """ - dir_path = os.path.dirname(path) - if not os.path.exists(dir_path): - os.makedirs(dir_path, exist_ok=True) - - with open(path, "w") as f: - f.write(str(os.getpid())) - - -def remove_lock_file(path): - """ - Removes a lock file if it exists. - """ - if os.path.exists(path): - os.remove(path) diff --git a/scripts/lib/fs.py b/scripts/lib/fs.py deleted file mode 100644 index 0fde6e1f5..000000000 --- a/scripts/lib/fs.py +++ /dev/null @@ -1,29 +0,0 @@ -# Deskflow -- mouse and keyboard sharing utility -# Copyright (C) 2024 Symless Ltd. -# -# This package is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# found in the file LICENSE that should have accompanied this file. -# -# This package is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import os, fnmatch - - -def find_files(search_dirs, include_files, exclude_dirs=[]): - """Recursively find files, excluding specified directories""" - matches = [] - for dir in search_dirs: - for root, dirnames, filenames in os.walk(dir): - dirnames[:] = [d for d in dirnames if d not in exclude_dirs] - - for pattern in include_files: - for filename in fnmatch.filter(filenames, pattern): - matches.append(os.path.join(root, filename)) - return matches diff --git a/scripts/lint_clang.py b/scripts/lint_clang.py deleted file mode 100755 index a2f31d5c5..000000000 --- a/scripts/lint_clang.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env python3 - -# Deskflow -- mouse and keyboard sharing utility -# Copyright (C) 2024 Symless Ltd. -# -# This package is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# found in the file LICENSE that should have accompanied this file. -# -# This package is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import lib.env as env - -env.ensure_in_venv(__file__) - -import argparse, sys -import lib.fs as fs -from clang_format import clang_format # type: ignore - -include_files = [ - "*.h", - "*.c", - "*.hpp", - "*.cpp", - "*.m", - "*.mm", -] - -dirs = ["src"] - - -def main(): - """ - Cross-platform equivalent of using find and xargs with clang-format. - Lints by performing a dry run (--dry-run) which fails when formatting is needed. - """ - parser = argparse.ArgumentParser() - parser.add_argument( - "-f", - "--format", - action="store_true", - help="In-place format all files", - ) - args = parser.parse_args() - - cmd_args = ["-i"] if args.format else ["--dry-run", "--Werror"] - files_recursive = fs.find_files(dirs, include_files) - - if args.format: - print("Formatting files with Clang formatter:") - else: - print("Checking files with Clang formatter:") - - for file in files_recursive: - print(file) - - if files_recursive: - sys.argv = [""] + cmd_args + files_recursive - result = clang_format() - if result == 0: - print("Clang lint passed") - - sys.exit(result) - else: - print("No files for Clang to process", file=sys.stderr) - sys.exit(0) - - -if __name__ == "__main__": - main() diff --git a/scripts/pyproject.toml b/scripts/pyproject.toml deleted file mode 100644 index 12fc4fad6..000000000 --- a/scripts/pyproject.toml +++ /dev/null @@ -1,10 +0,0 @@ -[project] -name = "scripts" -version = "0.0.1" -description = "Scripts to assist with development of Deskflow" -requires-python = ">=3.9" -dependencies = [ - "clang-format", - "python-dotenv", - "colorama", -] diff --git a/scripts/setup_venv.py b/scripts/setup_venv.py deleted file mode 100755 index d298b2954..000000000 --- a/scripts/setup_venv.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python3 - -# Deskflow -- mouse and keyboard sharing utility -# Copyright (C) 2024 Symless Ltd. -# -# This package is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# found in the file LICENSE that should have accompanied this file. -# -# This package is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import lib.env as env - -env.ensure_in_venv(__file__, create_venv=True) -env.install_requirements() - -import lib.colors as colors - -print(colors.SUCCESS_TEXT, "Python virtual environment is ready.") diff --git a/sonar-project.properties b/sonar-project.properties index 835098274..138720a59 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,9 +1,9 @@ sonar.organization=deskflow sonar.projectKey=deskflow_deskflow -sonar.sources=scripts,src/apps,src/lib +sonar.sources=src/apps,src/lib sonar.tests=src/test sonar.exclusions=subprojects/**,build/** -sonar.coverage.exclusions=subprojects/**,scripts/**,src/test/**,src/apps/deskflow-gui/**,src/apps/res/** +sonar.coverage.exclusions=subprojects/**,src/test/**,src/apps/deskflow-gui/**,src/apps/res/** sonar.cpd.exclusions=**/*Test*.cpp sonar.host.url=https://sonarcloud.io sonar.coverageReportPaths=${{ steps.coverage-paths.outputs.csv }}