refactor: move optional scripts to external repo
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@ -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
|
||||
|
||||
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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}"
|
||||
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
||||
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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()
|
||||
@ -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",
|
||||
]
|
||||
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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.")
|
||||
@ -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 }}
|
||||
|
||||
Reference in New Issue
Block a user