diff --git a/.ci/create-deb.sh b/.ci/create-deb.sh new file mode 100755 index 00000000..d3b8693c --- /dev/null +++ b/.ci/create-deb.sh @@ -0,0 +1,204 @@ +#!/bin/env sh + +# see https://github.com/raspiblitz/raspiblitz/blob/dev/build_sdcard.sh + +me="${0##/*}" +nocolor="\033[0m" +red="\033[31m" + +## usage as a function to be called whenever there is a huge mistake on the options +usage(){ + printf %s"${me} [--option ] + +Options: + -h, --help this help info + -a, --app-name the application name + -o, --output-dir the output directory + -v, --version the application version + -A, --architecture the application architecture + -m, --maintainer-name the application maintainer name + -e, --maintainer-email the application maintainer email + -d, --description the application description + -b, --binary the application binary + -i, --icon the application icon image + +Notes: + all options, long and short accept --opt=value mode also + [0|1] can also be referenced as [false|true] +" + exit 1 +} + +if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then + usage +fi + +## assign_value variable_name "${opt}" +## it strips the dashes and assign the clean value to the variable +## assign_value status --on IS status=on +## variable_name is the name you want it to have +## $opt being options with single or double dashes that don't require arguments +assign_value(){ + case "${2}" in + --*) value="${2#--}";; + -*) value="${2#-}";; + *) value="${2}" + esac + case "${value}" in + 0) value="false";; + 1) value="true";; + esac + ## Escaping quotes is needed because else if will fail if the argument is quoted + # shellcheck disable=SC2140 + eval "${1}"="\"${value}\"" +} + +## default user message +error_msg(){ printf %s"${red}${me}: ${1}${nocolor}\n"; exit 1; } + +## get_arg variable_name "${opt}" "${arg}" +## get_arg service --service ssh +## variable_name is the name you want it to have +## $opt being options with single or double dashes +## $arg is requiring and argument, else it fails +## assign_value "${1}" "${3}" means it is assining the argument ($3) to the variable_name ($1) +get_arg(){ + case "${3}" in + ""|-*) error_msg "Option '${2}' requires an argument.";; + esac + assign_value "${1}" "${3}" +} + +## hacky getopts +## 1. if the option requires an argument, and the option is preceeded by single or double dash and it +## can be it can be specified with '-s=ssh' or '-s ssh' or '--service=ssh' or '--service ssh' +## use: get_arg variable_name "${opt}" "${arg}" +## 2. if a bunch of options that does different things are to be assigned to the same variable +## and the option is preceeded by single or double dash use: assign_value variable_name "${opt}" +## as this option does not require argument, specifu $shift_n=1 +## 3. if the option does not start with dash and does not require argument, assign to command manually. +while :; do + case "${1}" in + -*=*) opt="${1%=*}"; arg="${1#*=}"; shift_n=1;; + -*) opt="${1}"; arg="${2}"; shift_n=2;; + *) opt="${1}"; arg="${2}"; shift_n=1;; + esac + case "${opt}" in + -a|-a=*|--app-name|--app-name=*) get_arg app_name "${opt}" "${arg}";; + -o|-o=*|--output-dir|--output-dir=*) get_arg output_dir "${opt}" "${arg}";; + -v|-v=*|--version|--version=*) get_arg version "${opt}" "${arg}";; + -A|-A=*|--architecture|--architecture=*) get_arg architecture "${opt}" "${arg}";; + -m|-m=*|--maintainer-name|--maintainer-name=*) get_arg maintainer_name "${opt}" "${arg}";; + -e|-e=*|--maintainer-email|--maintainer-email=*) get_arg maintainer_email "${opt}" "${arg}";; + -d|-d=*|--description|--description=*) get_arg description "${opt}" "${arg}";; + -b|-b=*|--binary|--binary=*) get_arg binary "${opt}" "${arg}";; + -i|-i=*|--icon|--icon=*) get_arg icon "${opt}" "${arg}";; + "") break;; + *) echo "Invalid option: ${opt}";; + esac + shift "${shift_n}" +done + + +echo "=================" +echo "create-deb v0.0.1" +echo "=================" +echo "" +echo "Defined variables" +echo "-----------------" +for key in app_name output_dir version architecture maintainer_name maintainer_email description binary icon; do + eval val='$'"${key}" + if [ -n "${val}" ]; then + printf '%s\n' "${key}=${val}" + elif [ -z "${val}"]; then + error_msg "${key} undefined" + fi +done + +# convert x86_64 to amd64 +if [ $architecture == "x86_64" ]; then + echo "" + echo "Replacing $architecture to amd64" + echo "--------------------------------" + architecture="amd64" +fi + +echo"" + +FULL_OUTPUT_PATH=${output_dir}/${app_name}_${version}_${architecture} +mkdir -v -p $FULL_OUTPUT_PATH +mkdir -v -p $FULL_OUTPUT_PATH/DEBIAN +mkdir -v -p $FULL_OUTPUT_PATH/usr/local/bin +mkdir -v -p $FULL_OUTPUT_PATH/usr/share/applications +mkdir -v -p $FULL_OUTPUT_PATH/usr/share/icons/hicolor/ +mkdir -v -p $FULL_OUTPUT_PATH/usr/share/icons/hicolor/512x512/ +mkdir -v -p $FULL_OUTPUT_PATH/usr/share/icons/hicolor/512x512/apps +cp -v ${binary} $FULL_OUTPUT_PATH/usr/local/bin/${app_name} +cp -v ${icon} $FULL_OUTPUT_PATH/usr/share/icons/hicolor/512x512/apps/${app_name}.png + +# create control file +cat < $FULL_OUTPUT_PATH/DEBIAN/control +Package: ${app_name} +Version: ${version} +Architecture: ${architecture} +Maintainer: ${maintainer_name} <${maintainer_email}> +Description: ${description} +EOF + +echo "" +echo "Resulting $FULL_OUTPUT_PATH/DEBIAN/control" +echo "----------------------------------------------------------------------" +cat $FULL_OUTPUT_PATH/DEBIAN/control + +# create postscript file +cat < $FULL_OUTPUT_PATH/DEBIAN/postinst +#!/bin/sh + +echo "" +echo " -------------" +echo " !!!WARNING!!!" +echo " -------------" +echo "" +if [ -n "\$SUDO_USER" ] && [ "\$SUDO_USER" != "root" ]; then + echo "Adding user \$SUDO_USER to 'dialout' group to enable flash procedure..." + echo "You'll need to reboot your system to enable changes" + usermod -a -G dialout \$SUDO_USER +elif [ -n "\$USER" ] && [ "\$USER" != "root"]; then + echo "Adding user \$USER to 'dialout' group to enable flash procedure..." + echo "You'll need to reboot your system to enable changes" + usermod -a -G dialout \$USER +fi +echo "" +echo "" +EOF +chmod 0755 $FULL_OUTPUT_PATH/DEBIAN/postinst + +echo "" +echo "Resulting $FULL_OUTPUT_PATH/DEBIAN/postinst" +echo "----------------------------------------------------------------------" +cat $FULL_OUTPUT_PATH/DEBIAN/postinst + +# create desktop entry +cat < $FULL_OUTPUT_PATH/usr/share/applications/${app_name}.desktop +[Desktop Entry] +Encoding=UTF-8 +Version=${version} +Type=Application +Terminal=false +Exec=/usr/local/bin/${app_name} +Name=${app_name} +Icon=/usr/share/icons/highcolor/512x512/apps/${app_name}.png +EOF + +echo "" +echo "Resulting $FULL_OUTPUT_PATH/usr/share/applications/${app_name}.desktop" +echo "-----------------------------------------------------------------------------------------------------" +cat $FULL_OUTPUT_PATH/usr/share/applications/${app_name}.desktop + +echo "" +echo "setting permissions for $FULL_OUTPUT_PATH/usr/local/bin/${app_name}" +chmod +x $FULL_OUTPUT_PATH/usr/local/bin/${app_name} + +# build .deb +echo "running dpkg-deb --build --root-owner-group $FULL_OUTPUT_PATH" +dpkg-deb --build --root-owner-group $FULL_OUTPUT_PATH diff --git a/.ci/create-nsis.py b/.ci/create-nsis.py new file mode 100644 index 00000000..95382751 --- /dev/null +++ b/.ci/create-nsis.py @@ -0,0 +1,324 @@ +# Miscelanious code from +# https://github.com/Thiagojm/NSIS-Script-Maker-for-Windows-/blob/main/nsis_template.txt +# https://gist.github.com/mattiasghodsian/a30f50568792939e35e93e6bc2084c2a +import os +import re +import platform +from argparse import ArgumentParser, Namespace +from sys import set_coroutine_origin_tracking_depth + +parser = ArgumentParser( + prog='create-nsis', + description='Create a Setup installer for your program', + usage="create-nsis [...options] | Out-File -FilePath -Encoding utf8 " +) + +parser.add_argument("-a", "--name", help="The name of your app") +parser.add_argument("-b", "--binary", help="The path of your app") +parser.add_argument("-o", "--organization", help="The name of your organization") +parser.add_argument("-d", "--description", help="The application description") +parser.add_argument("-V", "--app-version", help="The version of your application o x.y.z form") +parser.add_argument("-l", "--license", help="The path of your application license") +parser.add_argument("-i", "--icon", help="The icon path of your application") +parser.add_argument("-I", "--asset", action="append", help="The name of asset and the path in form of name:path") +parser.add_argument("-O", "--output", help="The folder where the the NSIS script will be put") + +def escape(message: str) -> str: + if platform.system() == "Windows": + return message.replace("/", "\\") + else: + return message + +def make_headers(args: Namespace) -> str: + print("* make headers") + return "\n".join([ + ";--------------------------------", + "!define MULTIUSER_EXECUTIONLEVEL Highest", + "!define MULTIUSER_MUI", + "", + ";---------------------------------", + "; Main header", + "!include \"MultiUser.nsh\"", + "!include \"MUI2.nsh\"", + "", + "" + ]) + +def make_defines(args: Namespace) -> str: + print("* make defines") + version = args.app_version.split(".") + binary = escape(args.binary) + license = escape(args.license) + icon = escape(args.icon) + install_size = os.path.getsize(args.binary) + install_size += os.path.getsize(args.icon) + install_size += os.path.getsize(args.license) + + text = [ + ";--------------------------------", + "; Custom define", + f"!define APP_NAME {args.name}", + f"!define ORG_NAME {args.organization}", + f"!define APP_DESC \"{args.description}\"", + f"!define APP_BINARY \"{binary}\"", + f"!define APP_VERSION_MAJOR {version[0]}", + f"!define APP_VERSION_MINOR {version[1]}", + f"!define APP_VERSION_BUILD {version[2]}", + f"!define APP_SLUG \"{args.name} v{args.app_version}\"", + f"!define APP_LICENSE \"{license}\"", + f"!define APP_ICON \"{icon}\"", + ] + + if args.asset is not None and len(args.asset) > 0: + for asset in args.asset: + _asset = asset.split(":") + _asset_name = _asset[0] + _asset_rev = escape(_asset[1]) + text.append(f"!define APP_ASSET_{_asset_name.upper()} \"{_asset_rev}\"") + + text.append("") + text.append("") + return "\n".join(text) + +def make_general(args: Namespace) -> str: + print("* make general") + return "\n".join([ + ";--------------------------------", + "; General", + "Name \"${APP_NAME}\"", + "OutFile \"${APP_NAME} Setup.exe\"", + "InstallDir \"$PROGRAMFILES\\${APP_NAME}\"", + "LicenseData \"${APP_LICENSE}\"", + "InstallDirRegKey HKCU \"Software\\${APP_NAME}\" \"\"", + "RequestExecutionLevel admin", + "Unicode true", + #"SilentInstall silent", + "", + "" + ]) + +def make_ui(args: Namespace) -> str: + print("* make UI") + return "\n".join([ + ";--------------------------------", + "; UI", + "!define MUI_ICON \"${APP_ICON}\"", + #"!define MUI_HEADERIMAGE", + #"!define MUI_WELCOMEFINISHPAGE_BITMAP \"assets\\logo.png\"", + #"!define MUI_HEADERIMAGE_BITMAP \"assets\\logo.png\"", + "!define MUI_ABORTWARNING", + "!define MUI_WELCOMEPAGE_TITLE \"${APP_SLUG} Setup\"", + "", + "" + ]) + +def make_pages(args: Namespace) -> str: + print("* make pages") + return "\n".join([ + ";--------------------------------", + "; Pages", + "!define MUI_FINISHPAGE_SHOWREADME \"\"", + "!define MUI_FINISHPAGE_SHOWREADME_NOTCHECKED", + "!define MUI_FINISHPAGE_SHOWREADME_TEXT \"Create Desktop/Menu entry shortcuts\"", + "!define MUI_FINISHPAGE_SHOWREADME_FUNCTION finishpageaction", + "!insertmacro MUI_PAGE_WELCOME", + "!insertmacro MULTIUSER_PAGE_INSTALLMODE", + "!insertmacro MUI_PAGE_LICENSE \"LICENSE\"", + "!insertmacro MUI_PAGE_COMPONENTS", + "!insertmacro MUI_PAGE_DIRECTORY", + "!insertmacro MUI_PAGE_INSTFILES", + "!insertmacro MUI_PAGE_FINISH", + "!insertmacro MUI_UNPAGE_CONFIRM", + "!insertmacro MUI_UNPAGE_INSTFILES", + "!define MUI_TEXT_WELCOME_INFO_TITLE \"Welcome to ${APP_SLUG} setup\"", + "!insertmacro MUI_LANGUAGE \"English\"", + "!define MUI_TEXT_WELCOME_INFO_TITLE \"Bem vindo à cconfiguração do ${APP_SLUG}\"", + "!insertmacro MUI_LANGUAGE \"Portuguese\"", + "!define MUI_TEXT_WELCOME_INFO_TITLE \"Bienvenido a la configuración de ${APP_SLUG}\"", + "!insertmacro MUI_LANGUAGE \"Spanish\"", + "!define MUI_TEXT_WELCOME_INFO_TITLE \"Benvenuto nell'installazione di ${APP_SLUG}\"", + "!insertmacro MUI_LANGUAGE \"Italian\"", + "!define MUI_TEXT_WELCOME_INFO_TITLE \"Bienvenue dans la configuration de ${APP_SLUG}\"", + "!insertmacro MUI_LANGUAGE \"French\"", + "!define MUI_TEXT_WELCOME_INFO_TITLE \"Welkom by ${APP_SLUG} Setup\"", + "!insertmacro MUI_LANGUAGE \"Afrikaans\"", + "!define MUI_TEXT_WELCOME_INFO_TITLE \"Добро пожаловать в ${APP_SLUG} Конфигурация\"", + "!insertmacro MUI_LANGUAGE \"Russian\"", + "", + "" + ]) + +def make_macro_verify_user_is_admin(args: Namespace) -> str: + print("* make macro VerifyUserIsAdmin") + return "\n".join([ + ";--------------------------------", + "; Macro verify user is admin", + "!include LogicLib.nsh", + "!macro VerifyUserIsAdmin", + "UserInfo::GetAccountType", + "pop $0", + ";Require admin rights on NT4+", + "${If} $0 != \"admin\"", + "\tmessageBox mb_iconstop \"Administrator rights required!\"", + "\t;ERROR_ELEVATION_REQUIRED", + "\tsetErrorLevel 740", + "\tquit", + "${EndIf}", + "!macroend", + "", + "" + ]) + +def make_on_init(args: Namespace) -> str: + print("* make function onInit") + return "\n".join([ + ";--------------------------------", + "; function on init", + "function .onInit", + "\tsetShellVarContext all", + "\t!insertmacro VerifyUserIsAdmin", + "\t!insertmacro MULTIUSER_INIT", + "functionEnd", + "", + "" + ]) + +def make_finish_page_action(args: Namespace) -> str: + print("* make function finishpageaction") + return "\n".join([ + ";--------------------------------", + "; function finishpageaction", + "function finishpageaction", + "\t; Start Menu", + "\tCreateDirectory \"$SMPROGRAMS\\${APP_NAME}\"", + "\tCreateShortCut \"$SMPROGRAMS\\${APP_NAME}.lnk\" \"$INSTDIR\\${APP_NAME}.exe\"", + "", + "\t; Desktop shortcut", + "\tCreateShortCut \"$DESKTOP\\${APP_NAME}.lnk\" \"$INSTDIR\\${APP_NAME}.exe\"", + "functionEnd", + "", + "", + ]) + +def make_install_section(args: Namespace) -> str: + print("* make install section") + text = "" + text += "\n".join([ + ";--------------------------------", + "; Section - Install App", + "Section \"install\" SEC_01", + #"\tSection \"-hidden install\"", + "\tSectionIn RO", + "\tSetOutPath \"$INSTDIR\"", + "", + "\t; Files added here should be removed by the uninstaller (see section 'uninstall')", + "\tFile /r \"${APP_BINARY}\"", + ]) + + _text = [] + + if args.asset is not None and len(args.asset) > 0: + for asset in args.asset: + _asset = asset.split(":") + _asset_name = _asset[0].upper() + _text.append("\tFile /r \"${APP_ASSET_" +_asset_name + "}\"") + + text += "\n".join(_text) + + text += "\n".join([ + "\t; Uninstaller - See function un.onInit and section 'uninstall' for configuration", + "\tWriteUninstaller \"$INSTDIR\\uninstall.exe\"", + "", + "\t; Registry information for add/remove programs", + "\tWriteRegStr HKCU \"Software\\${APP_NAME}\" \"\" $INSTDIR", + "\tWriteRegStr HKLM \"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${APP_NAME}\" \"DisplayName\" \"${APP_NAME} - ${APP_DESC}\"", + "\tWriteRegStr HKLM \"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${APP_NAME}\" \"UninstallString\" \"$\\\"$INSTDIR\\uninstall.exe$\\\"\"", + "\tWriteRegStr HKLM \"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${APP_NAME}\" \"QuietUninstallString\" \"$\\\"$INSTDIR\\uninstall.exe$\\\" /S\"", + "\tWriteRegStr HKLM \"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${APP_NAME}\" \"InstallLocation\" \"$\\\"$INSTDIR$\\\"\"", + "\tWriteRegStr HKLM \"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${APP_NAME}\" \"DisplayIcon\" \"$\\\"$INSTDIR\\${APP_ICON}$\\\"\"", + "\tWriteRegStr HKLM \"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${APP_NAME}\" \"Publisher\" \"$\\\"${ORG_NAME}$\\\"\"", + "\tWriteRegStr HKLM \"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${APP_NAME}\" \"DisplayVersion\" \"$\\\"${APP_VERSION_MAJOR}.${APP_VERSION_MINOR}.${APP_VERSION_BUILD}$\\\"\"", + "\tWriteRegDWORD HKLM \"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${APP_NAME}\" \"VersionMajor\" ${APP_VERSION_MAJOR}", + "\tWriteRegDWORD HKLM \"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${APP_NAME}\" \"VersionMinor\" ${APP_VERSION_MINOR}", + "\tWriteRegDWORD HKLM \"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${APP_NAME}\" \"VersionMinor\" ${APP_VERSION_MINOR}", + "", + "\t; There is no option for modifying or repairing the install", + "\tWriteRegDWORD HKLM \"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${APP_NAME}\" \"NoModify\" 1", + "\tWriteRegDWORD HKLM \"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${APP_NAME}\" \"NoRepair\" 1", + "", + "\t; Write MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_VALUENAME so the correct context can be detected in the uninstaller.", + "\tWriteRegStr ShCtx \"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${APP_NAME}\" $MultiUser.InstallMode 1", + "", + "\t; Obtain the size of the files, in kilobytes, in section SEC_01", + "\tSectionGetSize \"${SEC_01}\" $0", + "\tWriteRegDWORD HKLM \"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${APP_NAME}\" \"EstimatedSize\" $0", + "", + "\tWriteUninstaller \"$INSTDIR\\Uninstall.exe\"", + "SectionEnd", + "", + "" + + ]) + + return text + +def make_uninstaller(args: Namespace) -> str: + print("* make uninstaller section") + return "\n".join([ + ";--------------------------------", + "; Uninstaller", + "function un.onInit", + "\tSetShellVarContext all", + "\t; Verify the uninstaller - last chance to back out", + "\tMessageBox MB_OKCANCEL \"Permanantly remove ${APP_NAME}?\" IDOK next", + "\t\tAbort", + "\tnext:", + "\t!insertmacro VerifyUserIsAdmin", + "\t!insertmacro MULTIUSER_UNINIT", + "functionEnd", + "", + ";--------------------------------", + "; Uninstall section", + "Section \"uninstall\"", + "", + "", + "\t; Delete menu entries", + "\tDelete \"$SMPROGRAMS\\${APP_NAME}.lnk\"", + "\tRmDir /r \"$SMPROGRAMS\\${APP_NAME}\"", + "", + "\t; Delete desktop shortcut", + "\tDelete \"$DESKTOP\\${APP_NAME}.lnk\"", + "", + "\t; Remove files", + "\tDelete $INSTDIR\\*", + "\tDelete $INSTDIR\\uninstall.exe", + "", + "\t; Try to remove the install directory - this will only happen if it is empty", + "\tRmDir /r $INSTDIR", + "", + "\t; Remove uninstaller information from the registry", + "\tDeleteRegKey HKLM \"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${ORG_NAME} ${APP_NAME}\"", + "sectionEnd" + ]) + +args = parser.parse_args() + +try: + file = os.path.join(args.output, f"{args.name}.nsi") + + with open(file, mode="w", encoding="utf-8") as nsis_script: + nsis_script.write(make_headers(args)) + nsis_script.write(make_defines(args)) + nsis_script.write(make_general(args)) + nsis_script.write(make_ui(args)) + nsis_script.write(make_macro_verify_user_is_admin(args)) + nsis_script.write(make_finish_page_action(args)) + nsis_script.write(make_on_init(args)) + nsis_script.write(make_pages(args)) + nsis_script.write(make_install_section(args)) + nsis_script.write(make_uninstaller(args)) + + print(f"{file} created") + +except Exception as err: + print(err) diff --git a/.ci/create-rpm.sh b/.ci/create-rpm.sh new file mode 100755 index 00000000..10bda05a --- /dev/null +++ b/.ci/create-rpm.sh @@ -0,0 +1,232 @@ +#!/bin/env sh + +# see https://github.com/raspiblitz/raspiblitz/blob/dev/build_sdcard.sh + +me="${0##/*}" +nocolor="\033[0m" +red="\033[31m" + +## usage as a function to be called whenever there is a huge mistake on the options +usage(){ + printf %s"${me} [--option ] + +Options: + -h, --help this help info + -a, --name the application name + -v, --version the application version + -m, --maintainer-name the application maintainer name + -e, --maintainer-email the application maintainer email + -d, --description the application description + -r, --readme the application readme path + -c, --changelog the application changelog path + -b, --binary the application binary + -i, --icon the application icon image + +Notes: + all options, long and short accept --opt=value mode also + [0|1] can also be referenced as [false|true] +" + exit 1 +} + +if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then + usage +fi + +## assign_value variable_name "${opt}" +## it strips the dashes and assign the clean value to the variable +## assign_value status --on IS status=on +## variable_name is the name you want it to have +## $opt being options with single or double dashes that don't require arguments +assign_value(){ + case "${2}" in + --*) value="${2#--}";; + -*) value="${2#-}";; + *) value="${2}" + esac + case "${value}" in + 0) value="false";; + 1) value="true";; + esac + ## Escaping quotes is needed because else if will fail if the argument is quoted + # shellcheck disable=SC2140 + eval "${1}"="\"${value}\"" +} + +## default user message +error_msg(){ printf %s"${red}${me}: ${1}${nocolor}\n"; exit 1; } + +## get_arg variable_name "${opt}" "${arg}" +## get_arg service --service ssh +## variable_name is the name you want it to have +## $opt being options with single or double dashes +## $arg is requiring and argument, else it fails +## assign_value "${1}" "${3}" means it is assining the argument ($3) to the variable_name ($1) +get_arg(){ + case "${3}" in + ""|-*) error_msg "Option '${2}' requires an argument.";; + esac + assign_value "${1}" "${3}" +} + +## hacky getopts +## 1. if the option requires an argument, and the option is preceeded by single or double dash and it +## can be it can be specified with '-s=ssh' or '-s ssh' or '--service=ssh' or '--service ssh' +## use: get_arg variable_name "${opt}" "${arg}" +## 2. if a bunch of options that does different things are to be assigned to the same variable +## and the option is preceeded by single or double dash use: assign_value variable_name "${opt}" +## as this option does not require argument, specifu $shift_n=1 +## 3. if the option does not start with dash and does not require argument, assign to command manually. +while :; do + case "${1}" in + -*=*) opt="${1%=*}"; arg="${1#*=}"; shift_n=1;; + -*) opt="${1}"; arg="${2}"; shift_n=2;; + *) opt="${1}"; arg="${2}"; shift_n=1;; + esac + case "${opt}" in + -a|-a=*|--name|--name=*) get_arg name "${opt}" "${arg}";; + -v|-v=*|--version|--version=*) get_arg version "${opt}" "${arg}";; + -m|-m=*|--maintainer-name|--maintainer-name=*) get_arg maintainer_name "${opt}" "${arg}";; + -e|-e=*|--maintainer-email|--maintainer-email=*) get_arg maintainer_email "${opt}" "${arg}";; + -d|-d=*|--description|--description=*) get_arg description "${opt}" "${arg}";; + -c|-c=*|--changelog|--changelog=*) get_arg changelog "${opt}" "${arg}";; + -r|-r=*|--readme|--readme=*) get_arg readme "${opt}" "${arg}";; + -b|-b=*|--binary|--binary=*) get_arg binary "${opt}" "${arg}";; + -i|-i=*|--icon|--icon=*) get_arg icon "${opt}" "${arg}";; + "") break;; + *) echo "Invalid option: ${opt}";; + esac + shift "${shift_n}" +done + + +echo "=================" +echo "create-rpm v0.0.1" +echo "=================" +echo "" +echo "Defined variables" +echo "-----------------" +for key in name version maintainer_name maintainer_email description binary icon changelog readme; do + eval val='$'"${key}" + if [ -n "${val}" ]; then + printf '%s\n' "${key}=${val}" + elif [ -z "${val}"]; then + error_msg "${key} undefined" + fi +done + +echo"" + +# follow https://www.redhat.com/sysadmin/create-rpm-package +RELEASE=1 +RPM_NAME=${name}-${version} +BUILD_PATH=$HOME/rpmbuild +TAR_PATH=$BUILD_PATH/$RPM_NAME +CHANGELOG=$(cat $changelog) + +mkdir -v -p $RPM_NAME +mkdir -v -p $BUILD_PATH/ +mkdir -v -p $BUILD_PATH/BUILD +mkdir -v -p $BUILD_PATH/RPMS +mkdir -v -p $BUILD_PATH/SOURCES +mkdir -v -p $BUILD_PATH/SPECS +mkdir -v -p $BUILD_PATH/SRPMS + +# Place the script in the designated directory +cp -v $binary $RPM_NAME +cp -v $icon $RPM_NAME/${name}.png +cp -v $readme $RPM_NAME/README +tar -v --create --file $RPM_NAME.tar.gz $RPM_NAME +mv $RPM_NAME.tar.gz $BUILD_PATH/SOURCES + +# Create a .spec file +cat < $BUILD_PATH/SPECS/${name}.spec +Name: ${name} +Version: ${version} +Release: ${RELEASE}%{?dist} +Summary: ${description} +Group: application +BuildArch: %{_arch} +License: MIT +URL: https://github.com/selfcustody/krux-installer +Source0: %{name}-%{version}.tar.gz + +%description +${description} + +%prep +%setup -q + +%files +%{_bindir}/%{name} +%{_datadir}/doc/%{name}/README +%{_datarootdir}/applications/%{name}.desktop +%{_datarootdir}/icons/highcolor/512x512/apps/%{name}.png + +%install +mkdir -p %{buildroot} +mkdir -p %{buildroot}%{_bindir} +mkdir -p %{buildroot}%{_datadir}/doc/%{name} +mkdir -p %{buildroot}%{_datarootdir}/applications/%{name} +mkdir -p %{buildroot}%{_datarootdir}/icons/highcolor/512x512/apps +cp %{name} %{buildroot}%{_bindir} +cp README %{buildroot}%{_datadir}/doc/%{name}/README +cp %{name}.png %{buildroot}%{_datarootdir}/icons/highcolor/512x512/apps/%{name}.png +echo "[Desktop Entry]" > %{buildroot}%{_datarootdir}/applications/%{name}.desktop +echo "Encoding=UTF-8" >> %{buildroot}%{_datarootdir}/applications/%{name}.desktop +echo "Version=%{version}" >> %{buildroot}%{_datarootdir}/applications/%{name}.desktop +echo "Type=Application" >> %{buildroot}%{_datarootdir}/applications/%{name}.desktop +echo "Terminal=false" >> %{buildroot}%{_datarootdir}/applications/%{name}.desktop +echo "Exec=%{_bindir}/%{name}" >> %{buildroot}%{_datarootdir}/applications/%{name}.desktop +echo "Name=%{name}" >> %{buildroot}%{_datarootdir}/applications/%{name}.desktop +echo "Icon=%{_datarootdir}/icons/highcolor/512x512/apps/%{name}.png" >> %{buildroot}%{_datarootdir}/applications/%{name}.desktop + +%clean +rm -rf %{buildroot} + +%changelog +* $(LC_ALL=en_US.utf8 date +'%a %b %d %Y') ${maintainer_name} <${maintainer_email}> - ${version}-1 +${CHANGELOG} + +%post +echo "" +echo " -------------" +echo " !!!WARNING!!!" +echo " -------------" +echo "" +if [ -n "\$SUDO_USER" ] && [ "\$SUDO_USER" != "root" ]; then + echo "Adding user \$SUDO_USER to 'dialout' group to enable flash procedure..." + echo "You'll need to reboot your system to enable changes" + usermod -a -G dialout \$SUDO_USER +elif [ -n "\$USER" ] && [ "\$USER" != "root"]; then + echo "Adding user \$USER to 'dialout' group to enable flash procedure..." + echo "You'll need to reboot your system to enable changes" + usermod -a -G dialout \$USER +fi +echo "" +echo "" + +%postun +rm -v %{_datarootdir}/applications/%{name}.desktop +rm -v %{_datarootdir}/icons/highcolor/512x512/apps/%{name}.png +echo "" +echo " -------------" +echo " !!!WARNING!!!" +echo " -------------" +echo "" +if [ -n "\$SUDO_USER" ] && [ "\$SUDO_USER" != "root" ]; then + echo "Removing user \$SUDO_USER from 'dialout' group to disable flash procedure..." + echo "You'll need to reboot your system to enable changes" + usermod -a -G dialout \$SUDO_USER +elif [ -n "\$USER" ] && [ "\$USER" != "root"]; then + echo "Removing user \$USER from 'dialout' group to disable flash procedure..." + echo "You'll need to reboot your system to enable changes" + usermod -a -G dialout \$USER +fi +echo "" +echo "" +EOF + +echo "Resulting $BUILD_PATH/SPECS/${name}.spec" +echo "---------------------------------------------------" +cat $BUILD_PATH/SPECS/${name}.spec diff --git a/.ci/create-spec.py b/.ci/create-spec.py new file mode 100755 index 00000000..ba511923 --- /dev/null +++ b/.ci/create-spec.py @@ -0,0 +1,121 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +build.py +""" +from re import findall +from os import listdir +from os.path import join, isfile +from pathlib import Path +from platform import system +import argparse +import PyInstaller.building.makespec + +if __name__ == "__main__": + p = argparse.ArgumentParser() + PyInstaller.building.makespec.__add_options(p) + PyInstaller.log.__add_options(p) + + SYSTEM = system() + + # build executable for following systems + if SYSTEM not in ("Linux", "Windows", "Darwin"): + raise OSError(f"OS '{system()}' not implemented") + + # Get root path to properly setup + DIR = Path(__file__).parents + ROOT_PATH = Path(__file__).parent.parent.absolute() + PYNAME = "krux-installer" + PYFILE = f"{PYNAME}.py" + KFILE = str(ROOT_PATH / PYFILE) + ASSETS = str(ROOT_PATH / "assets") + ICON = join(ASSETS, "icon.png") + I18NS = str(ROOT_PATH / "src" / "i18n") + + BUILDER_ARGS = [ ] + + # The app name + BUILDER_ARGS.append(f"--name={PYNAME}") + + # The application has window + BUILDER_ARGS.append("--windowed") + + # Icon + BUILDER_ARGS.append(f"--icon={ICON}") + + # Specifics about operational system + # on how will behave as file or bundled app + if SYSTEM == "Linux": + # Tha application is a GUI + BUILDER_ARGS.append("--onefile") + + elif SYSTEM == "Windows": + # Tha application is a GUI with a hidden console + # to keep `sys` module enabled (necessary for Kboot) + BUILDER_ARGS.append("--onefile") + BUILDER_ARGS.append("--console") + BUILDER_ARGS.append("--hide-console=minimize-early") + + elif SYSTEM == "Darwin": + # Tha application is a GUI in a bundled .app + BUILDER_ARGS.append("--onefile") + BUILDER_ARGS.append("--noconsole") + + # For darwin system, will be necessary + # to add a hidden import for ssl + # (necessary for request module) + BUILDER_ARGS.append("--hidden-import=ssl") + BUILDER_ARGS.append("--hidden-import=pillow") + BUILDER_ARGS.append("--optimize=2") + + # Necessary for get version and + # another infos in application + BUILDER_ARGS.append("--add-data=pyproject.toml:.") + + # some assets + for f in listdir(ASSETS): + asset = join(ASSETS, f) + if isfile(asset): + if asset.endswith("png") or asset.endswith("gif") or asset.endswith("ttf"): + BUILDER_ARGS.append(f"--add-data={asset}:assets") + + # Add i18n translations + for f in listdir(I18NS): + i18n_abs = join(I18NS, f) + i18n_rel = join("src", "i18n") + if isfile(i18n_abs): + if findall(r"^[a-z]+\_[A-Z]+\.UTF-8\.json$", f): + BUILDER_ARGS.append(f"--add-data={i18n_abs}:{i18n_rel}") + + + args = p.parse_args(BUILDER_ARGS) + + # Now generate spec + print("============================") + print("create-spec.py") + print("============================") + print() + for k, v in vars(args).items(): + print(f"{k}: {v}") + + print() + PyInstaller.building.makespec.main(["krux-installer.py"], **vars(args)) diff --git a/.ci/edit-spec.ps1 b/.ci/edit-spec.ps1 new file mode 100755 index 00000000..93159940 --- /dev/null +++ b/.ci/edit-spec.ps1 @@ -0,0 +1,36 @@ +# Build process for pyinstaller/kivy +# will require some editions and, do it mannually +# is something impossible on github actions +# so we do it as script +# see more at +# https://kivy.org/doc/stable/guide/packaging-windows.html + +# Now we need to edit the spec file to add the dependencies hooks to correctly build the exe. +# Open the spec file with your favorite editor and add these lines at the beginning of the spec +$header = "# -*- mode: python ; coding: utf-8 -*-" +$new_header = "# -*- mode: python ; coding: utf-8 -*-`r`nfrom kivy_deps import sdl2, glew" + +# which we will edit to add the dependencies. +# In this instance, edit the arguments to the EXE command +$old_code = @" +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='krux-installer', +"@ + +$new_code = @" +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + *[Tree(p) for p in (sdl2.dep_bins + glew.dep_bins)], + name='krux-installer', +"@ + +# Now do the .spec edition and build the .exe +(Get-Content -Raw .\krux-installer.spec).replace($header, $new_header).replace($old_code, $new_code) | Set-Content .\krux-installer.spec \ No newline at end of file diff --git a/.ci/patch-pyinstaller-kivy-hook.ps1 b/.ci/patch-pyinstaller-kivy-hook.ps1 new file mode 100644 index 00000000..282fd93a --- /dev/null +++ b/.ci/patch-pyinstaller-kivy-hook.ps1 @@ -0,0 +1,41 @@ +[String]$PATCHURL = "https://raw.githubusercontent.com/ikus060/kivy/21c7110ee79f355d6a42da0a274d2426b1e18665/kivy/tools/packaging/pyinstaller_hooks/__init__.py" +echo "env PATCHURL=$PATCHURL" + +[String]$PYENV_PATH = poetry.exe env info --path +echo "env PYENV_PATH=$PYENV_PATH" + +[String]$PYENV_VERSION = python -c "import sys; t= sys.version_info[:]; print(f'{t[0]}.{t[1]}')" +echo "env PYENV_VERSION=$PYENV_VERSION" + +[String]$FULL_PATH = "$PYENV_PATH\Lib\site-packages\kivy\tools\packaging\pyinstaller_hooks\__init__.py" +echo "env FULL_PATH=$FULL_PATH" + +[String]$GIT_TOOLS_PATH = "$Env:PROGRAMFILES\Git\usr\bin" +echo "env GIT_TOOLS_PATH=$GIT_TOOLS_PATH" + +[String]$DIFF_PATH = "$GIT_TOOLS_PATH\diff.exe" +echo "env DIFF_PATH=$DIFF_PATH" + +[String]$PATCH_PATH = "$GIT_TOOLS_PATH\patch.exe" +echo "env PATCH_PATH=$PATCH_PATH" + +[String]$HOOK_PY_PATH = "$pwd\pyinstaller_hook_patch.py" +echo "env HOOK_PY_PATH=$HOOK_PY_PATH" + +[String]$HOOK_PATCH_PATH = "$pwd\pyinstaller_hook.patch" +echo "env HOOK_PATCH_PATH=$HOOK_PATCH_PATH" + +echo "RUN wget $PATCHURL -OutFile $HOOK_PY_PATH" +wget $PATCHURL -OutFile $HOOK_PY_PATH + +echo "RUN '$DIFF_PATH' -u $FULL_PATH $HOOK_PY_PATH | Set-Content $HOOK_PATCH_PATH" +& "$DIFF_PATH" -u $FULL_PATH $HOOK_PY_PATH | Set-Content $HOOK_PATCH_PATH + +echo "RUN '$PATCH_PATH' -su -i $HOOK_PATCH_PATH '$FULL_PATH'" +& "$PATCH_PATH" -su -i $HOOK_PATCH_PATH "$FULL_PATH" + +echo "RUN Remove-Item -Path $HOOK_PY_PATH" +Remove-Item -Path $HOOK_PY_PATH + +echo "RUN Remove-Item -Path $HOOK_PATCH_PATH" +Remove-Item -Path $HOOK_PATCH_PATH diff --git a/.ci/patch-pyinstaller-kivy-hook.sh b/.ci/patch-pyinstaller-kivy-hook.sh new file mode 100755 index 00000000..78ce7de3 --- /dev/null +++ b/.ci/patch-pyinstaller-kivy-hook.sh @@ -0,0 +1,32 @@ +#!/bin/env sh + +# download pyinstaller-kivy patch +PATCHURL="https://raw.githubusercontent.com/ikus060/kivy/21c7110ee79f355d6a42da0a274d2426b1e18665/kivy/tools/packaging/pyinstaller_hooks/__init__.py" +echo "env PATCHURL=$PATCHURL" + +PYENV_PATH=`poetry env info --path` +echo "env PYENV_PATH=$PYENV_PATH" + +PYENV_VERSION=`python -c 'import sys; t= sys.version_info[:]; print(f"{t[0]}.{t[1]}")'` +echo "env PYENV_VERSION=$PYENV_VERSION" + +PYHOOK_PATH="site-packages/kivy/tools/packaging/pyinstaller_hooks/__init__.py" +FULL_PATH=$PYENV_PATH/lib/python$PYENV_VERSION/$PYHOOK_PATH +echo "env FULL_PATH=$FULL_PATH" + +wget $PATCHURL -O pyinstaller_hook_patch.py +echo "RUN wget $PATCHURL -O pyinstaller_hook_patch.py" + +echo "RUN diff -u $FULL_PATH pyinstaller_hook_patch.py > pyinstaller_hook.patch" +diff -u $FULL_PATH pyinstaller_hook_patch.py > pyinstaller_hook.patch + +# patch it +echo "RUN patch $FULL_PATH < pyinstaller_hook.patch" +patch $FULL_PATH < pyinstaller_hook.patch + +# remove remaining files +rm pyinstaller_hook_patch.py +echo "RUN rm pyinstaller_hook_patch.py" + +rm pyinstaller_hook.patch +echo "RUN rm pyinstaller_hook.patch" diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 2bc0b863..f8dab1b2 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -9,3 +9,4 @@ assignees: '' --- #### Describe the bug + diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md new file mode 100644 index 00000000..82c7456b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.md @@ -0,0 +1,12 @@ +--- + +name: 📚 Documentation +about: The project need some better docs +title: "[Documentation] The title of issue" +labels: documentation +assignees: '' + +--- + +#### Describe the documentation + diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md new file mode 100644 index 00000000..a0e26e0c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -0,0 +1,11 @@ +--- +name: 🔥 Enhancement +about: New feature or request +title: "[Enhancement] the title of new feature or enhancement" +labels: enhancement +assignees: '' + +--- + +#### Describe the new feature or enhancement + diff --git a/.github/ISSUE_TEMPLATE/help_wanted.md b/.github/ISSUE_TEMPLATE/help_wanted.md index 6fba797e..9ef5cf43 100644 --- a/.github/ISSUE_TEMPLATE/help_wanted.md +++ b/.github/ISSUE_TEMPLATE/help_wanted.md @@ -1,6 +1,6 @@ --- name: 🥺 Help wanted -about: Confuse about the use of electron-vue-vite +about: Confuse about something on project title: "[Help] the title of help wanted report" labels: help wanted assignees: '' @@ -8,3 +8,4 @@ assignees: '' --- #### Describe the problem you confuse + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index c533dfbf..00000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,12 +0,0 @@ - - -### Description - - - -### What is the purpose of this pull request? - -- [ ] Bug fix -- [ ] New Feature -- [ ] Documentation update -- [ ] Other diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 00000000..db921cb1 --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,6 @@ +coverage: + status: + project: + default: + target: 100% + threshold: 15% diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fe250ebd..aa05c8cf 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,7 +5,12 @@ version: 2 updates: - - package-ecosystem: "npm" # See documentation for possible values - directory: "/" # Location of package manifests + - package-ecosystem: "github-actions" + directory: "/" schedule: - interval: "monthly" + interval: "weekly" + + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/build-linux-appimage.yml b/.github/workflows/build-linux-appimage.yml deleted file mode 100644 index b957eb2e..00000000 --- a/.github/workflows/build-linux-appimage.yml +++ /dev/null @@ -1,73 +0,0 @@ -name: Build electron application as AppImage on linux - -on: - workflow_call: - secrets: - token: - required: true - -jobs: - - build-linux: - runs-on: ubuntu-latest - - steps: - - - name: Checkout Git repository - uses: actions/checkout@v3 - - - name: Install node - uses: actions/setup-node@v4 - with: - node-version: "20.11.1" - - - name: Variables helpers - id: setup - run: | - KRUX_VERSION=`node -e "console.log(require('./package.json').version)"` - echo "app-version=$KRUX_VERSION" >> $GITHUB_OUTPUT - KRUX_NAME=krux-installer-$KRUX_VERSION - echo "app-name=$KRUX_NAME" >> $GITHUB_OUTPUT - echo "::group::Variables" - echo "app-version=$KRUX_VERSION" - echo "app-name=$KRUX_NAME" - echo "::endgroup::" - - - name: Install dependencies - run: yarn install - - - name: Build electron app - env: - GH_TOKEN: ${{ secrets.token }} - run: yarn run build --linux AppImage - - - name: Hash electron app (Linux) - uses: qlrd/sha256sum-action@v2 - with: - working-directory: ./release/${{ steps.setup.outputs.app-version }} - file: ${{ steps.setup.outputs.app-name }}.AppImage - ext: sha256.txt - - - name: List release files - run: | - echo "::group::Release files" - ls -la release/${{ steps.setup.outputs.app-version }} - echo "::endgroup::" - - #- name: Install xvfb-maybe to allow headless test - # run: yarn add --dev xvfb-maybe - - #- name: E2E test electron app - # env: - # DEBUG: 'krux:*' - # run: ./node_modules/.bin/xvfb-maybe ./node_modules/.bin/wdio run wdio.conf.mts - - - name: Upload artifacts - uses: actions/upload-artifact@v3 - if: ${{ github.ref_name == 'main' }} - with: - name: ${{ runner.os }}-${{ steps.setup.outputs.app-name }}-AppImage - retention-days: 5 - path: | - release/${{ steps.setup.outputs.app-version }}/${{ steps.setup.outputs.app-name }}.AppImage - release/${{ steps.setup.outputs.app-version }}/${{ steps.setup.outputs.app-name }}.AppImage.sha256.txt diff --git a/.github/workflows/build-linux-deb-arm64.yml b/.github/workflows/build-linux-deb-arm64.yml deleted file mode 100644 index 4d16a3ed..00000000 --- a/.github/workflows/build-linux-deb-arm64.yml +++ /dev/null @@ -1,82 +0,0 @@ -name: Build electron application as deb package on linux (arm64) - -on: - workflow_call: - secrets: - DOCKER_USERNAME: - required: true - DOCKER_PASSWORD: - required: true - token: - required: true - -jobs: - - build-linux-deb-arm64: - runs-on: ubuntu-latest - env: - DOCKER_IMAGE: krux-installer-deb - DOCKER_TARGET_PLATFORM: ubuntu/arm64/v8 - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - steps: - - uses: actions/checkout@v2 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - - name: Cache Docker layers - uses: actions/cache@v2 - with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-buildx- - - - name: Setup variables - id: setup - run: | - echo "docker-platform=${DOCKER_TARGET_PLATFORM}" >> $GITHUB_OUTPUT - echo "docker-image=${DOCKER_IMAGE}/${DOCKER_TARGET_PLATFORM}" >> $GITHUB_OUTPUT - echo "docker-version=${GITHUB_RUN_NUMBER}" >> $GITHUB_OUTPUT - KRUX_VERSION=`node -e "console.log(require('./package.json').version)"` - echo "app-version=$KRUX_VERSION" >> $GITHUB_OUTPUT - echo "app-name=krux-installer_${KRUX_VERSION}_arm64" >> $GITHUB_OUTPUT - - - name: Docker Login - run: echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin - - - name: Create release folder - run: mkdir -p ./release/${{ steps.setup.outputs.app-version }} - - - name: Run Buildx - run: | - docker buildx build \ - --file ./dockerfiles/deb/Dockerfile \ - --platform ${{ steps.setup.outputs.docker-platform }} \ - --tag ${{ steps.setup.outputs.docker-image }}:${{ steps.setup.outputs.docker-version }} \ - --output type=local,dest=./release . - - - name: Hash electron app - uses: qlrd/sha256sum-action@v2 - with: - working-directory: release/${{ steps.setup.outputs.app-version }} - file: ${{ steps.setup.outputs.app-name }}.deb - ext: sha256.txt - - - name: List releases - run: ls ./release/${{ steps.setup.outputs.app-version }} - - - name: Upload artifacts - uses: actions/upload-artifact@v3 - if: ${{ github.ref_name == 'main' }} - with: - name: ${{ runner.os }}-${{ steps.setup.outputs.app-name }}-deb - retention-days: 5 - path: | - release/${{ steps.setup.outputs.app-version }}/${{ steps.setup.outputs.app-name }}.deb - release/${{ steps.setup.outputs.app-version }}/${{ steps.setup.outputs.app-name }}.deb.sha256.txt diff --git a/.github/workflows/build-linux-deb.yml b/.github/workflows/build-linux-deb.yml deleted file mode 100644 index bbd05489..00000000 --- a/.github/workflows/build-linux-deb.yml +++ /dev/null @@ -1,73 +0,0 @@ -name: Build electron application as deb package on linux - -on: - workflow_call: - secrets: - token: - required: true - -jobs: - - build-linux: - runs-on: ubuntu-latest - - steps: - - - name: Checkout Git repository - uses: actions/checkout@v3 - - - name: Install node - uses: actions/setup-node@v4 - with: - node-version: "20.11.1" - - - name: Variables helpers - id: setup - run: | - KRUX_VERSION=`node -e "console.log(require('./package.json').version)"` - echo "app-version=$KRUX_VERSION" >> $GITHUB_OUTPUT - KRUX_NAME=krux-installer - echo "app-name=${KRUX_NAME}_${KRUX_VERSION}_amd64" >> $GITHUB_OUTPUT - echo "::group::Variables" - echo "app-version=$KRUX_VERSION" - echo "app-name=$KRUX_NAME" - echo "::endgroup::" - - - name: Install dependencies - run: yarn install - - - name: Build electron app - env: - GH_TOKEN: ${{ secrets.token }} - run: yarn run build --linux deb - - - name: Hash electron app (Linux) - uses: qlrd/sha256sum-action@v2 - with: - working-directory: ./release/${{ steps.setup.outputs.app-version }} - file: ${{ steps.setup.outputs.app-name }}.deb - ext: sha256.txt - - - name: List release files - run: | - echo "::group::Release files" - ls -la release/${{ steps.setup.outputs.app-version }} - echo "::endgroup::" - - #- name: Install xvfb-maybe to allow headless test - # run: yarn add --dev xvfb-maybe - - #- name: E2E test electron app - # env: - # DEBUG: 'krux:*' - # run: ./node_modules/.bin/xvfb-maybe ./node_modules/.bin/wdio run wdio.conf.mts - - - name: Upload artifacts - uses: actions/upload-artifact@v3 - if: ${{ github.ref_name == 'main' }} - with: - name: ${{ runner.os }}-${{ steps.setup.outputs.app-name }}-deb - retention-days: 5 - path: | - release/${{ steps.setup.outputs.app-version }}/${{ steps.setup.outputs.app-name }}.deb - release/${{ steps.setup.outputs.app-version }}/${{ steps.setup.outputs.app-name }}.deb.sha256.txt diff --git a/.github/workflows/build-linux-rpm-arm64.yml b/.github/workflows/build-linux-rpm-arm64.yml deleted file mode 100644 index e691b04d..00000000 --- a/.github/workflows/build-linux-rpm-arm64.yml +++ /dev/null @@ -1,82 +0,0 @@ -name: Build electron application as rpm package on linux (arm64) - -on: - workflow_call: - secrets: - DOCKER_USERNAME: - required: true - DOCKER_PASSWORD: - required: true - token: - required: true - -jobs: - - build-linux-rpm-arm64: - runs-on: ubuntu-latest - env: - DOCKER_IMAGE: krux-installer-rpm - DOCKER_TARGET_PLATFORM: ubuntu/arm64/v8 - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - steps: - - uses: actions/checkout@v2 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - - name: Cache Docker layers - uses: actions/cache@v2 - with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-buildx- - - - name: Setup variables - id: setup - run: | - echo "docker-platform=${DOCKER_TARGET_PLATFORM}" >> $GITHUB_OUTPUT - echo "docker-image=${DOCKER_IMAGE}/${DOCKER_TARGET_PLATFORM}" >> $GITHUB_OUTPUT - echo "docker-version=${GITHUB_RUN_NUMBER}" >> $GITHUB_OUTPUT - KRUX_VERSION=`node -e "console.log(require('./package.json').version)"` - echo "app-version=$KRUX_VERSION" >> $GITHUB_OUTPUT - echo "app-name=krux-installer-${KRUX_VERSION}.aarch64" >> $GITHUB_OUTPUT - - - name: Docker Login - run: echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin - - - name: Create release folder - run: mkdir -p ./release/${{ steps.setup.outputs.app-version }} - - - name: Run Buildx - run: | - docker buildx build \ - --file ./dockerfiles/rpm/Dockerfile \ - --platform ${{ steps.setup.outputs.docker-platform }} \ - --tag ${{ steps.setup.outputs.docker-image }}:${{ steps.setup.outputs.docker-version }} \ - --output type=local,dest=./release . - - - name: Hash electron app - uses: qlrd/sha256sum-action@v2 - with: - working-directory: release/${{ steps.setup.outputs.app-version }} - file: ${{ steps.setup.outputs.app-name }}.rpm - ext: sha256.txt - - - name: List releases - run: ls ./release/${{ steps.setup.outputs.app-version }} - - - name: Upload artifacts - uses: actions/upload-artifact@v3 - if: ${{ github.ref_name == 'main' }} - with: - name: ${{ runner.os }}-${{ steps.setup.outputs.app-name }}-rpm - retention-days: 5 - path: | - release/${{ steps.setup.outputs.app-version }}/${{ steps.setup.outputs.app-name }}.rpm - release/${{ steps.setup.outputs.app-version }}/${{ steps.setup.outputs.app-name }}.rpm.sha256.txt diff --git a/.github/workflows/build-linux-rpm.yml b/.github/workflows/build-linux-rpm.yml deleted file mode 100644 index 481ea7a5..00000000 --- a/.github/workflows/build-linux-rpm.yml +++ /dev/null @@ -1,76 +0,0 @@ -name: Build electron application as rpm package on linux - -on: - workflow_call: - secrets: - token: - required: true - -jobs: - - build-linux: - runs-on: ubuntu-latest - - steps: - - - name: Checkout Git repository - uses: actions/checkout@v3 - - - name: Install RPM dependencies - run: sudo apt-get install rpm - - - name: Install node - uses: actions/setup-node@v3 - with: - node-version: "20.10.0" - - - name: Variables helpers - id: setup - run: | - KRUX_VERSION=`node -e "console.log(require('./package.json').version)"` - echo "app-version=$KRUX_VERSION" >> $GITHUB_OUTPUT - KRUX_NAME=krux-installer - echo "app-name=${KRUX_NAME}-${KRUX_VERSION}.x86_64" >> $GITHUB_OUTPUT - echo "::group::Variables" - echo "app-version=$KRUX_VERSION" - echo "app-name=$KRUX_NAME" - echo "::endgroup::" - - - name: Install dependencies - run: yarn install - - - name: Build electron app - env: - GH_TOKEN: ${{ secrets.token }} - run: yarn run build --linux rpm - - - name: Hash electron app (Linux) - uses: qlrd/sha256sum-action@v2 - with: - working-directory: ./release/${{ steps.setup.outputs.app-version }} - file: ${{ steps.setup.outputs.app-name }}.rpm - ext: sha256.txt - - - name: List release files - run: | - echo "::group::Release files" - ls -la release/${{ steps.setup.outputs.app-version }} - echo "::endgroup::" - - #- name: Install xvfb-maybe to allow headless test - # run: yarn add --dev xvfb-maybe - - #- name: E2E test electron app - # env: - # DEBUG: 'krux:*' - # run: ./node_modules/.bin/xvfb-maybe ./node_modules/.bin/wdio run wdio.conf.mts - - - name: Upload artifacts - uses: actions/upload-artifact@v3 - if: ${{ github.ref_name == 'main' }} - with: - name: ${{ runner.os }}-${{ steps.setup.outputs.app-name }}-rpm - retention-days: 5 - path: | - release/${{ steps.setup.outputs.app-version }}/${{ steps.setup.outputs.app-name }}.rpm - release/${{ steps.setup.outputs.app-version }}/${{ steps.setup.outputs.app-name }}.rpm.sha256.txt diff --git a/.github/workflows/build-mac-dmg.yml b/.github/workflows/build-mac-dmg.yml deleted file mode 100644 index e9ba22d3..00000000 --- a/.github/workflows/build-mac-dmg.yml +++ /dev/null @@ -1,114 +0,0 @@ -name: Build electron application as dmg package on MacOS - -on: - workflow_call: - secrets: - token: - required: true - -jobs: - - build-macos-12: - runs-on: macos-latest - - steps: - - - name: Checkout Git repository - uses: actions/checkout@v3 - - - name: Install node - uses: actions/setup-node@v3 - with: - node-version: "20.10.0" - - - name: Variables helpers - id: setup - run: | - KRUX_VERSION=`node -e "console.log(require('./package.json').version)"` - echo "app-version=$KRUX_VERSION" >> $GITHUB_OUTPUT - KRUX_NAME=krux-installer_$KRUX_VERSION - echo "app-name=$KRUX_NAME" >> $GITHUB_OUTPUT - echo "::group::Variables" - echo "app-version=$KRUX_VERSION" - echo "app-name=$KRUX_NAME" - echo "::endgroup::" - - - name: Install node-gyp deps - run: | - python3 --version - #mkdir .krux-installer-node-gyp - #python3 -m venv .krux-installer-node-gyp - #source .krux-installer-node-gyp/bin/activate - #python3 -m pip install distutils - #python3 -m pip install setuptools - brew install python-setuptools - - - name: Install dependencies - run: yarn install - - #- name: Install chromedriver - # env: - # CHROMEDRIVER_VERSION: '114.0.5735.90' - # ZIPFILE: 'chromedriver_mac64.zip' - # DESTDIR: './node_modules/chromedriver/bin' - # ZIPURI: 'https://chromedriver.storage.googleapis.com' - # run: | - # curl -o ${TMPDIR}${ZIPFILE} $ZIPURI/$CHROMEDRIVER_VERSION/$ZIPFILE - # unzip -o ${TMPDIR}${ZIPFILE} -d $DESTDIR - - #- name: List chromedriver binaries - # run: ls -la node_modules/chromedriver/bin - - #- name: Pre-test chromedriver - # run: ./node_modules/chromedriver/bin/chromedriver --version - - - name: Build electron app - env: - GH_TOKEN: ${{ secrets.token }} - run: yarn run build --macos dmg - - - name: Hash electron app - uses: qlrd/sha256sum-action@v2 - with: - working-directory: ./release/${{ steps.setup.outputs.app-version }} - file: ${{ steps.setup.outputs.app-name }}.dmg - ext: sha256.txt - - #- name: List release files - # run: | - # echo "::group::Release files" - # ls -la release/${{ steps.setup.outputs.app-version }} - # echo "::endgroup::" - # echo "::group::Unpacked files" - # ls -la release/${{ steps.setup.outputs.app-version }}/mac - # echo "::endgroup::" - # echo "::group::Unpacked .app directory" - # ls -la release/${{ steps.setup.outputs.app-version }}/mac/krux-installer.app - # echo "::endgroup::" - # echo "::group::Unpacked .app/Contents directory" - # ls -la release/${{ steps.setup.outputs.app-version }}/mac/krux-installer.app/Contents - # echo "::endgroup::" - # echo "::group::Unpacked .app/Contents/Frameworks directory" - # ls -la release/${{ steps.setup.outputs.app-version }}/mac/krux-installer.app/Contents/Frameworks - # echo "::endgroup::" - # echo "::group::Unpacked .app/Contents/MacOS directory" - # ls -la release/${{ steps.setup.outputs.app-version }}/mac/krux-installer.app/Contents/MacOS - # echo "::endgroup::" - # echo "::group::Unpacked .app/Contents/Resources directory" - # ls -la release/${{ steps.setup.outputs.app-version }}/mac/krux-installer.app/Contents/Resources - # echo "::endgroup::" - - #- name: E2E test electron app - # env: - # DEBUG: 'krux:*' - # run: ./node_modules/.bin/wdio run wdio.conf.mts - - - name: Upload artifacts - uses: actions/upload-artifact@v3 - #if: ${{ github.ref_name == 'main' }} - with: - name: ${{ runner.os }}-${{ steps.setup.outputs.app-name }}-dmg - retention-days: 5 - path: | - release/${{ steps.setup.outputs.app-version }}/${{ steps.setup.outputs.app-name }}.dmg - release/${{ steps.setup.outputs.app-version }}/${{ steps.setup.outputs.app-name }}.dmg.sha256.txt diff --git a/.github/workflows/build-windows-nsis.yml b/.github/workflows/build-windows-nsis.yml deleted file mode 100644 index 59f3163b..00000000 --- a/.github/workflows/build-windows-nsis.yml +++ /dev/null @@ -1,178 +0,0 @@ -name: Build electron application on windows - -on: - workflow_call: - secrets: - token: - required: true - -jobs: - build-win: - runs-on: windows-latest - - steps: - - name: Checkout Git repository - uses: actions/checkout@v3 - - - name: Install node - uses: actions/setup-node@v3 - with: - node-version: "20.11.1" - - - name: Variables helpers - id: setup - shell: pwsh - run: | - $loc = Get-Location - $firmware_version = "v23.09.1" - $zipname = "krux-$firmware_version.zip" - $signame = "krux-$firmware_version.zip.sig" - $pemname = "selfcustody.pem" - $extraResources = "$loc\extraResources" - $opensslVersion = "3.3.1" - $release_url = "https://github.com/selfcustody/krux/releases/download" - $raw_url = "https://raw.githubusercontent.com/selfcustody/krux/main" - $app_version = node -e "console.log(require('./package.json').version)" - $pre_app_name = node -e "console.log(require('./package.json').name)" - $app_name=$pre_app_name + "_" + $app_version - echo "app-version=$app_version" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append - echo "app-name=$app_name" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append - echo "zip-file=$loc\$zipname" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append - echo "sig-file=$loc\$signame" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append - echo "pem-file=$loc\$pemname" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append - echo "release-zip=$release_url/$firmware_version/$zipname" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append - echo "release-sig=$release_url/$firmware_version/$signame" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append - echo "release-pem=$raw_url/$pemname" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append - echo "extra-resources=$extraResources" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append - echo "openssl-version=$opensslVersion" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append - echo "openssl-build-dir=$loc\openssl" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append - echo "openssl-prefix=$extraResources\OpenSSL" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append - echo "openssl-dir=$extraResources\OpenSSL\CommonFiles" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append - echo "openssl-bin=$prefix\bin\openssl.exe" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append - echo "::group::Variables" - echo "app-version=$app_version" - echo "app-name=$app_name" - echo "zip-file=$loc\$zipname" - echo "sig-file=$loc\$signame" - echo "pem-file=$loc\$pemname" - echo "release-zip=$release_url/$firmware_version/$zipname" - echo "release-sig=$release_url/$firmware_version/$signame" - echo "release-pem=$raw_url/$pemname" - echo "extra-resources=$extraResources" - echo "openssl-version=$opensslVersion" - echo "openssl-build-dir=$loc\openssl" - echo "openssl-prefix=$extraResources\OpenSSL" - echo "openssl-dir=$extraResources\OpenSSL\CommonFiles" - echo "openssl-bin=$prefix\bin\openssl.exe" - echo "::endgroup::" - - - name: Restore OpenSSL build - id: restore-cache-extra - uses: actions/cache/restore@v3 - with: - path: ${{ steps.setup.outputs.extra-resources }} - key: ${{ runner.os }}-${{ steps.setup.outputs.app-name }}-openssl-${{ steps.setup.outputs.openssl-version }} - - - name: Create extraResources directory for OpenSSL build - if: ${{ steps.restore-cache-extra.outputs.cache-hit != 'true' }} - shell: pwsh - run: | - $exists=Test-Path -Path ${{ steps.setup.outputs.extra-resources }} - if(!$exists) { New-Item -ItemType directory -Path ${{ steps.setup.outputs.extra-resources }} } - - - name: Configure, build, test and install OpenSSL - if: ${{ steps.restore-cache-extra.outputs.cache-hit != 'true' }} - uses: qlrd/compile-openssl-windows-action@v0.0.2 - with: - build-type: 'plain' - version: openssl-${{ steps.setup.outputs.openssl-version }} - prefix: ${{ steps.setup.outputs.openssl-prefix }} - openssldir: ${{ steps.setup.outputs.openssl-dir }} - - - name: Download krux's release firmware zip file for pre-test - if: ${{ steps.restore-cache-extra.outputs.cache-hit != 'true' }} - shell: cmd - run: curl.exe -L ${{ steps.setup.outputs.release-zip }} -o ${{ steps.setup.outputs.zip-file }} - - - name: Download krux's release firmware signature file for pre-test - if: ${{ steps.restore-cache-extra.outputs.cache-hit != 'true' }} - shell: cmd - run: curl.exe -L ${{ steps.setup.outputs.release-sig }} -o ${{ steps.setup.outputs.sig-file }} - - - name: Download selfcustody's public key certificate for pre-test - if: ${{ steps.restore-cache-extra.outputs.cache-hit != 'true' }} - shell: cmd - run: | - curl.exe -L -H "Accept-Charset: utf-8" ${{ steps.setup.outputs.release-pem }} -o ${{ steps.setup.outputs.pem-file }} - - - name: Pre-test built-in OpenSSL - if: ${{ steps.restore-cache-extra.outputs.cache-hit != 'true' }} - shell: cmd - run: > - ${{ steps.setup.outputs.openssl-prefix }}${{ steps.setup.outputs.openssl-bin }} sha256 <${{ steps.setup.outputs.zip-file }} -binary | - ${{ steps.setup.outputs.openssl-prefix }}${{ steps.setup.outputs.openssl-bin }} pkeyutl -verify -pubin - -inkey ${{ steps.setup.outputs.pem-file }} - -sigfile ${{ steps.setup.outputs.sig-file }} - - - name: Save cached built-in OpenSSL - if: ${{ steps.restore-cache-extra.outputs.cache-hit != 'true' }} - uses: actions/cache/save@v3 - with: - path: ${{ steps.setup.outputs.extra-resources }} - key: ${{ runner.os }}-${{ steps.setup.outputs.app-name }}-openssl-${{ steps.setup.outputs.openssl-version }} - - - name: Install dependencies - shell: pwsh - run: yarn.cmd install - - #- name: Install chromedriver.exe - # shell: pwsh - # run: | - # $url = "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/120.0.6099.56/win32/chrome-win32.zip" - # $tmp_path = ".\chromedriver_win32.zip" - # $dest_path = "node_modules\chromedriver\bin" - # Invoke-WebRequest -Uri $url -OutFile $tmp_path - # Expand-Archive -LiteralPath $tmp_path -DestinationPath $dest_path - - #- name: List chromedriver binaries - # shell: pwsh - # run: ls node_modules\chromedriver\bin - - - name: Build electron app - shell: pwsh - env: - GH_TOKEN: ${{ secrets.token }} - run: yarn.cmd run build --win nsis - - - name: Hash electron app - uses: qlrd/sha256sum-action@v2 - with: - working-directory: release/${{ steps.setup.outputs.app-version }} - file: ${{ steps.setup.outputs.app-name }}.exe - ext: sha256.txt - - - name: List release files - shell: pwsh - run: | - echo "::group::Release files" - ls release/${{ steps.setup.outputs.app-version }} - echo "::endgroup::" - echo "::group::Win Unpacked files" - ls release/${{ steps.setup.outputs.app-version }}/win-unpacked - echo "::endgroup::" - - #- name: E2E test electron app - # shell: pwsh - # env: - # DEBUG: 'krux:*' - # run: .\node_modules\.bin\wdio.cmd run wdio.conf.mts - - - name: Upload artifacts - uses: actions/upload-artifact@v3 - if: ${{ github.ref_name == 'main' }} - with: - name: ${{ runner.os}}-${{ steps.setup.outputs.app-name }}-Nsis - retention-days: 5 - path: | - release/${{ steps.setup.outputs.app-version }}/${{ steps.setup.outputs.app-name }}.exe - release/${{ steps.setup.outputs.app-version }}/${{ steps.setup.outputs.app-name }}.exe.sha256.txt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index cd5df577..00000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Build - -on: - push: - branches: - - main - paths-ignore: - - "**.md" - #- "**.spec.js" - - ".idea" - - ".vscode" - - ".dockerignore" - - "Dockerfile" - - ".gitignore" - #- ".github/**" - #- "!.github/workflows/build.yml" - -jobs: - - build-linux-appimage: - uses: ./.github/workflows/build-linux-appimage.yml - secrets: - token: ${{ secrets.github_token }} - - build-linux-deb: - uses: ./.github/workflows/build-linux-deb.yml - secrets: - token: ${{ secrets.github_token }} - - #build-linux-deb-arm64: - # uses: ./.github/workflows/build-linux-deb-arm64.yml - # secrets: - # token: ${{ secrets.github_token }} - # DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - # DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - #build-linux-rpm-arm64: - # uses: ./.github/workflows/build-linux-rpm-arm64.yml - # secrets: - # token: ${{ secrets.github_token }} - # DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - # DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - build-linux-rpm: - uses: ./.github/workflows/build-linux-rpm.yml - secrets: - token: ${{ secrets.github_token }} - - build-windows-nsis: - uses: ./.github/workflows/build-windows-nsis.yml - secrets: - token: ${{ secrets.github_token }} - - build-mac-dmg: - uses: ./.github/workflows/build-mac-dmg.yml - secrets: - token: ${{ secrets.github_token }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 003356d7..f41f638d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,10 +24,12 @@ jobs: list-files: json filters: | change: - - 'package-lock.json' - - 'yarn.lock' - - 'pnpm-lock.yaml' - + - '.gitignore' + - '.pylintrc' + - 'TODO.md' + - 'CHANGELOG.md' + - 'LICENSE' + # ref: https://github.com/github/docs/blob/main/.github/workflows/triage-unallowed-contributions.yml - name: Comment About Changes We Can't Accept if: ${{ steps.filter_not_allowed.outputs.change == 'true' }} @@ -37,9 +39,11 @@ jobs: let workflowFailMessage = "It looks like you've modified some files that we can't accept as contributions." try { const badFilesArr = [ - 'package-lock.json', - 'yarn.lock', - 'pnpm-lock.yaml', + '.gitignore', + '.pylintrc', + 'TODO.md', + 'CHANGELOG.md', + 'LICENSE' ] const badFiles = badFilesArr.join('\n- ') const reviewMessage = `👋 Hey there kruxer. It looks like you've modified some files that we can't accept as contributions. The complete list of files we can't accept are:\n- ${badFiles}\n\nYou'll need to revert all of the files you changed in that list using [GitHub Desktop](https://docs.github.com/en/free-pro-team@latest/desktop/contributing-and-collaborating-using-github-desktop/managing-commits/reverting-a-commit) or \`git checkout origin/main \`. Once you get those files reverted, we can continue with the review process. :octocat:\n\nMore discussion:\n- https://github.com/electron-vite/electron-vite-vue/issues/192` @@ -78,4 +82,4 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Lint markdown - run: npx markdownlint-cli ${{ needs.job1.outputs.markdown_files }} --ignore node_modules \ No newline at end of file + run: npx markdownlint-cli ${{ needs.job1.outputs.markdown_files }} --ignore node_modules diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..655df317 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,405 @@ +name: Tests + +on: + push: + branches: + - main + + pull_request: + +jobs: + + black: + + runs-on: ubuntu-latest + steps: + + - uses: actions/checkout@v4 + + - name: Install poetry + run: pipx install poetry + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: 'poetry' + cache-dependency-path: './poetry.lock' + architecture: x64 + + - name: Install poetry dependencies + run: poetry install + + - name: Check format/ + run: | + poetry run poe format-src --check --verbose + poetry run poe format-tests --check --verbose + poetry run poe format-e2e --check --verbose + poetry run poe format-installer --check --verbose + + pylint: + + needs: black + + runs-on: ubuntu-latest + + steps: + + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install poetry + run: pipx install poetry + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: 'poetry' + cache-dependency-path: './poetry.lock' + architecture: x64 + + - name: Install poetry dependencies + run: poetry install + + - name: Lint src/ + run: poetry run poe lint + + # from xPsycHoWasPx in discord chat: + # "running macOS in none gpu accelerated mode, and that means no OpenGL, + # no OpenGL no kivy window…. if u need to use vm, you also need a dedicated + # seperate supported GPU passed through to the osx VM .." + # + # TODO: find how to install properly libs for M1/M2 Macs + # they raise exceptions that numpy and others libs + # arent compiled for arm64 (macos-14 and macos-xlarge-*) + + pytest: + + needs: pylint + + strategy: + matrix: + include: + - os: ubuntu-latest + arch: x64 + + - os: windows-latest + arch: x64 + + - os: macos-13 + arch: x64 + + - os: macos-14 + arch: arm64 + + runs-on: ${{ matrix.os }} + + steps: + + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install poetry + run: pipx install poetry + + - uses: actions/setup-python@v5 + if: ${{ matrix.arch != 'arm64' }} + with: + python-version: '3.12' + cache: 'poetry' + cache-dependency-path: './poetry.lock' + architecture: ${{ matrix.arch }} + + - uses: actions/setup-python@v5 + if: ${{ matrix.arch == 'arm64' }} + with: + python-version: '3.12' + + - name: Install project and its dependencies + run: poetry install + + - name: Run tests with coverage (Linux) + if: ${{ runner.os == 'Linux' }} + uses: coactions/setup-xvfb@6b00cf1889f4e1d5a48635647013c0508128ee1a + with: + run: | + poetry add pytest-xvfb + poetry run poe coverage + + - name: Upload coverage reports to Codecov with GitHub Action + uses: codecov/codecov-action@v4 + if: ${{ runner.os == 'Linux' }} + with: + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true + + - name: Run tests (MacOS) + if: ${{ runner.os == 'macOS' }} + run: poetry run poe test-unit + + - name: Run tests (Windows) + if: ${{ runner.os == 'Windows' }} + env: + KIVY_GL_BACKEND: 'angle_sdl2' + run: poetry run poe test + + #- name: Run tests (MacOS) + # if: ${{ runner.os == 'macOS' }} + # env: + # KIVY_GL_DEBUG: 1 + # KIVY_GL_BACKEND: 'gl' + # run: poetry run poe test + + build: + + needs: pytest + + strategy: + matrix: + include: + - os: ubuntu-latest + arch: x64 + + - os: windows-latest + arch: x64 + + - os: macos-13 + arch: x64 + + - os: macos-14 + arch: arm64 + + runs-on: ${{ matrix.os }} + + steps: + + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install poetry + run: pipx install poetry + + - uses: actions/setup-python@v5 + if: ${{ matrix.arch != 'arm64' }} + with: + python-version: '3.12' + cache: 'poetry' + cache-dependency-path: './poetry.lock' + architecture: ${{ matrix.arch }} + + - uses: actions/setup-python@v5 + if: ${{ matrix.arch == 'arm64' }} + with: + python-version: '3.12' + + - name: Setup (Linux) + if: ${{ runner.os == 'Linux' }} + id: setup-linux + run: | + sudo apt-get install tree rpm + mkdir -p ./release + NAME="$(poetry run python -c 'from src.utils.constants import get_name; print(get_name())')" + VERSION="$(poetry run python -c 'from src.utils.constants import get_version; print(get_version())')" + DESCRIPTION="$(poetry run python -c 'from src.utils.constants import get_description; print(get_description())')" + echo "name=${NAME}" >> $GITHUB_OUTPUT + echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "description=${DESCRIPTION}" >> $GITHUB_OUTPUT + + - name: Setup (MacOS) + if: ${{ runner.os == 'macOS' }} + id: setup-macos + run: | + brew install create-dmg + brew reinstall openssl@3 + brew unlink openssl@3 && brew link openssl@3 + #OPENSSL_MAJOR_VERSION=`$(which openssl) -version | awk '{ print $2}' | cut -d . -f1` + OPENSSL_FULL_VERSION=`$(which openssl) -version | awk '{ print $2}'` + OPENSSL_PATH="/opt/homebrew/Cellar/openssl@3/${OPENSSL_FULL_VERSION}" + echo "dyld-path=${OPENSSL_PATH}/lib" >> $GITHUB_OUTPUT + + - name: Install project and its dependencies + run: poetry install + + - name: Patch pyinstaller_hook for kivy in Unix + if: ${{ runner.os == 'Linux' || runner.os == 'macOS' }} + run: poetry run poe patch-nix + + - name: Patch pyinstaller_hook for kivy in Windows + if: ${{ runner.os == 'Windows' }} + run: poetry run poe patch-win + + - name: Build dist (Linux) + if: ${{ runner.os == 'Linux' }} + uses: coactions/setup-xvfb@90473c3ebc69533a1a6e0505c36511b69c8c3135 + with: + run: | + poetry add pytest-xvfb + poetry run python .ci/create-spec.py + poetry run python -m PyInstaller krux-installer.spec + + - name: Build dist (MacOS) + if: ${{ runner.os == 'macOS' }} + env: + DYLD_LIBRARY_PATH: ${{ steps.setup-macos.outputs.dyld-path }} + run: | + poetry run poe clean-mac + poetry run python .ci/create-spec.py + poetry run python -m PyInstaller krux-installer.spec + + - name: Build dist (Windows) + if: ${{ runner.os == 'Windows' }} + env: + KIVY_GL_BACKEND: 'angle_sdl2' + run: | + poetry run python .ci/create-spec.py + .\.ci\edit-spec.ps1 + poetry run python -m PyInstaller krux-installer.spec + + - name: Build release deb (Linux) + if: ${{ runner.os == 'Linux' }} + id: release-deb + env: + NAME: ${{ steps.setup-linux.outputs.name }} + VERSION: ${{ steps.setup-linux.outputs.version }} + DESCRIPTION: ${{ steps.setup-linux.outputs.description }} + run: | + sh .ci/create-deb.sh \ + -a "${NAME}" \ + -o ./release \ + -v $VERSION \ + -A amd64 \ + -m qlrd \ + -e qlrddev@gmail.com \ + -d "${DESCRIPTION}" \ + -b "./dist/${NAME}" \ + -i ./assets/icon.png + tree ./release + echo "build-path=$(pwd)/release" >> $GITHUB_OUTPUT + echo "pkg=${NAME}_${VERSION}_amd64.deb" >> $GITHUB_OUTPUT + + - name: Build release rpm (Linux) + if: ${{ runner.os == 'Linux' }} + id: release-rpm + env: + NAME: ${{ steps.setup-linux.outputs.name }} + VERSION: ${{ steps.setup-linux.outputs.version }} + DESCRIPTION: ${{ steps.setup-linux.outputs.description }} + run: | + RPM_VERSION=$(sed -e 's/-/_/g' <<< $VERSION) + sh .ci/create-rpm.sh \ + -a "${NAME}" \ + -v $RPM_VERSION \ + -m qlrd \ + -e qlrddev@gmail.com \ + -d "${DESCRIPTION}" \ + -c ./CHANGELOG.md \ + -r ./README.md \ + -b "./dist/${NAME}" \ + -i ./assets/icon.png + tree ./release + rpmbuild -vv -bb --define "_bindir /usr/local/bin" $HOME/rpmbuild/SPECS/$NAME.spec + cp $HOME/rpmbuild/RPMS/x86_64/${NAME}-${RPM_VERSION}-1.x86_64.rpm ./release/${NAME}-${RPM_VERSION}-1.x86_64.rpm4 + echo "build-path=${HOME}/rpmbuild/RPMS/x86_64" >> $GITHUB_OUTPUT + echo "pkg=${NAME}-${RPM_VERSION}-1.x86_64.rpm" >> $GITHUB_OUTPUT + + - name: Build release dmg (MacOS) + if: ${{ runner.os == 'macOS' }} + id: release-macos + run: | + NAME="$(poetry run python -c 'from src.utils.constants import get_name; print(get_name())')" + VER="$(poetry run python -c 'from src.utils.constants import get_version; print(get_version())')" + ARCH="$(uname -m)" + mkdir -p ./release + create-dmg --volname "${NAME}" --volicon ./assets/icon.icns --window-pos 200 120 --window-size 800 400 --icon-size 100 --icon "${NAME}.app" 200 190 --app-drop-link 600 185 "./release/${NAME}_${VER}_${ARCH}.dmg" "./dist/${NAME}.app" + echo "build-path=$(pwd)/release" >> $GITHUB_OUTPUT + echo "pkg=${NAME}_${VER}_${ARCH}.dmg" >> $GITHUB_OUTPUT + + - name: Build release NSIS (Windows) + if: ${{ runner.os == 'Windows' }} + id: release-windows + shell: pwsh + run: | + choco --yes install nsis + New-Item ".\release" -Type Directory + $name = poetry run python -c 'from src.utils.constants import get_name; print(get_name())' + $version = poetry run python -c 'from src.utils.constants import get_version; print(get_version())' + $description = poetry run python -c 'from src.utils.constants import get_description; print(get_description())' + poetry run python .ci/create-nsis.py -a $name -b .\dist\krux-installer.exe -o selfcustody -V $version -d "$description" -l .\LICENSE -i .\assets\icon.ico -O . + makensis.exe .\krux-installer.nsi + Move-Item -Path ".\krux-installer Setup.exe" -Destination ".\release\krux-installer_v$version Setup.exe" + echo "build-path=$pwd\release" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append + echo "pkg=krux-installer_v$version Setup.exe" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append + + - name: Hash (Linux deb) + if: ${{ runner.os == 'Linux' }} + uses: qlrd/sha256sum-action@v3 + id: hash-deb + with: + working-directory: ${{ steps.release-deb.outputs.build-path }} + file: ${{ steps.release-deb.outputs.pkg }} + ext: 'sha256.txt' + + - name: Hash (Linux rpm) + if: ${{ runner.os == 'Linux' }} + uses: qlrd/sha256sum-action@v3 + id: hash-rpm + with: + working-directory: ${{ steps.release-rpm.outputs.build-path }} + file: ${{ steps.release-rpm.outputs.pkg }} + ext: 'sha256.txt' + + - name: Hash (MacOS) + if: ${{ runner.os == 'macOS' }} + uses: qlrd/sha256sum-action@v3 + id: hash-macos + with: + working-directory: ${{ steps.release-macos.outputs.build-path }} + file: ${{ steps.release-macos.outputs.pkg }} + ext: 'sha256.txt' + + - name: Hash (Windows) + if: ${{ runner.os == 'Windows' }} + uses: qlrd/sha256sum-action@v3 + id: hash-win + with: + working-directory: ${{ steps.release-windows.outputs.build-path }} + file: ${{ steps.release-windows.outputs.pkg }} + ext: 'sha256.txt' + + - name: Upload artifact deb (Linux) + if: ${{ runner.os == 'Linux' }} + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.release-deb.outputs.pkg }} + path: | + ${{ steps.release-deb.outputs.build-path}}/${{ steps.release-deb.outputs.pkg }} + ${{ steps.release-deb.outputs.build-path}}/${{ steps.release-deb.outputs.pkg }}.sha256.txt + + - name: Upload artifact rpm (Linux) + if: ${{ runner.os == 'Linux' }} + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.release-rpm.outputs.pkg }} + path: | + ${{ steps.release-rpm.outputs.build-path}}/${{ steps.release-rpm.outputs.pkg }} + ${{ steps.release-rpm.outputs.build-path}}/${{ steps.release-rpm.outputs.pkg }}.sha256.txt + + - name: Upload artifacts (MacOS) + if: ${{ runner.os == 'macOS' }} + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.release-macos.outputs.pkg }} + path: | + ${{ steps.release-macos.outputs.build-path}}/${{ steps.release-macos.outputs.pkg }} + ${{ steps.release-macos.outputs.build-path}}/${{ steps.release-macos.outputs.pkg }}.sha256.txt + + - name: Upload artifacts (Windows) + if: ${{ runner.os == 'Windows' }} + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.release-windows.outputs.pkg }} + path: | + ${{ steps.release-windows.outputs.build-path}}/${{ steps.release-windows.outputs.pkg }} + ${{ steps.release-windows.outputs.build-path}}/${{ steps.release-windows.outputs.pkg }}.sha256.txt diff --git a/.gitignore b/.gitignore index f798fbc2..f6874752 100644 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,92 @@ -# Logs -logs +# Compiled Sources +################### +*.o +*.a +*.elf +*.bin +*.map +*.hex +*.kff +*.dis +*.exe +*.bak +*.nsi + +# Assets for test +################# +*.pem +*.sig + +# Packages +############ + +# Logs and Databases +###################### *.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -dist-electron -release -*.local - -# Editor directories and files -.vscode/.debug.env -.idea + +# VIM Swap Files +###################### +*.swp + +# Build directories +###################### +release/ +build/ +build-*/ +dist/ +*.spec +kivy/ + +# Test failure outputs +###################### +tests/results/* + +# Python cache files +###################### +__pycache__/ +.pytest_cache +*.pyc +*.egg-info + +# Customized Makefile/project overrides +###################### +GNUmakefile +user.props + +# Generated rst files +###################### +genrst/ + +# MacOS desktop metadata files +###################### .DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? - -# lockfile -package-lock.json -pnpm-lock.yaml -yarn.lock \ No newline at end of file + +.vagrant + +.config.mk +.flash.conf.json +in.txt +out.txt +sitemap.xml.gz +*.bin +*.bin.sig +*.bin.sha256.txt +*.kfpkg +*.kfpkg.sig +*.kfpkg.sha256.txt +*.zip +*.zip.sig +*.zip.sha256.txt +memzip-files.c +memzip-files.zip +.coverage +privkey.pem +pubkey.pem +simulator/krux-screenshots +simulator/screenshots +simulator/sd + +!tests/firmware-v0.0.0.bin +!tests/firmware-v0.0.0.bin.badsig +!tests/firmware-v0.0.0.bin.sha256.txt +!tests/firmware-v0.0.0.bin.sig diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..2bd2c336 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/utils/kboot"] + path = src/utils/kboot + url = https://github.com/selfcustody/Kboot.git diff --git a/.pylint/src b/.pylint/src new file mode 100644 index 00000000..fd2d1378 --- /dev/null +++ b/.pylint/src @@ -0,0 +1,639 @@ +[MAIN] + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Clear in-memory caches upon conclusion of linting. Useful if running pylint +# in a server-like mode. +clear-cache-post-run=no + +# Load and enable all available extensions. Use --list-extensions to see a list +# all available extensions. +#enable-all-extensions= + +# In error mode, messages with a category besides ERROR or FATAL are +# suppressed, and no reports are done by default. Error mode is compatible with +# disabling specific errors. +#errors-only= + +# Always return a 0 (non-error) status code, even if lint errors are found. +# This is primarily useful in continuous integration scripts. +#exit-zero= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist=cv2 + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold under which the program will exit with error. +fail-under=10 + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +#from-stdin= + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\\' represents the directory delimiter on Windows systems, +# it can't be used as an escape character. +ignore-paths= + +# Files or directories matching the regular expression patterns are skipped. +# The regex matches against base names, not paths. The default value ignores +# Emacs file locks +ignore-patterns=^\.# + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.11 + +# Discover python modules and packages in the file system subtree. +recursive=no + +# Add paths to the list of the source roots. Supports globbing patterns. The +# source root is an absolute path or a path relative to the current working +# directory used to determine a package namespace for modules located under the +# source root. +source-roots= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# In verbose mode, extra non-checker-related info will be displayed. +#verbose= + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. If left empty, argument names will be checked with the set +# naming style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. If left empty, class names will be checked with the set naming style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. If left empty, function names will be checked with the set +# naming style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Regular expression matching correct type alias names. If left empty, type +# alias names will be checked with the set naming style. +#typealias-rgx= + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. If left empty, variable names will be checked with the set +# naming style. +#variable-rgx= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + asyncSetUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=8 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=16 + +# Maximum number of parents for a class (see R0901). +max-parents=8 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=builtins.BaseException,builtins.Exception + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow explicit reexports by alias from a package __init__. +allow-reexport-from-package=no + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence=HIGH, + CONTROL_FLOW, + INFERENCE, + INFERENCE_FAILURE, + UNDEFINED + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then re-enable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + use-implicit-booleaness-not-comparison-to-string, + use-implicit-booleaness-not-comparison-to-zero + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable= + + +[METHOD_ARGS] + +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +notes-rgx= + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + +# Let 'consider-using-join' be raised when the separator to join on would be +# non-empty (resulting in expected fixes of the type: ``"- " + " - +# ".join(items)``) +suggest-join-with-non-empty-separator=yes + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each +# category, as well as 'statement' which is the total number of statements +# analyzed. This score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +msg-template= + +# Set the output format. Available formats are: text, parseable, colorized, +# json2 (improved json format), json (old json format) and msvs (visual +# studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +#output-format= + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=yes + +# Signatures are removed from the similarity computation +ignore-signatures=yes + +# Minimum lines number of a similarity. +min-similarity-lines=12 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. No available dictionaries : You need to install +# both the python package and the system dependency for enchant to work. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members=cv2 + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins=no-member, + not-async-context-manager, + not-context-manager, + attribute-defined-outside-init + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io diff --git a/.pylint/tests b/.pylint/tests new file mode 100644 index 00000000..69e87ef8 --- /dev/null +++ b/.pylint/tests @@ -0,0 +1,644 @@ +[MAIN] + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Clear in-memory caches upon conclusion of linting. Useful if running pylint +# in a server-like mode. +clear-cache-post-run=no + +# Load and enable all available extensions. Use --list-extensions to see a list +# all available extensions. +#enable-all-extensions= + +# In error mode, messages with a category besides ERROR or FATAL are +# suppressed, and no reports are done by default. Error mode is compatible with +# disabling specific errors. +#errors-only= + +# Always return a 0 (non-error) status code, even if lint errors are found. +# This is primarily useful in continuous integration scripts. +#exit-zero= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold under which the program will exit with error. +fail-under=10 + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +#from-stdin= + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\\' represents the directory delimiter on Windows systems, +# it can't be used as an escape character. +ignore-paths= + +# Files or directories matching the regular expression patterns are skipped. +# The regex matches against base names, not paths. The default value ignores +# Emacs file locks +ignore-patterns=^\.# + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.11 + +# Discover python modules and packages in the file system subtree. +recursive=no + +# Add paths to the list of the source roots. Supports globbing patterns. The +# source root is an absolute path or a path relative to the current working +# directory used to determine a package namespace for modules located under the +# source root. +source-roots= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# In verbose mode, extra non-checker-related info will be displayed. +#verbose= + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. If left empty, argument names will be checked with the set +# naming style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. If left empty, class names will be checked with the set naming style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. If left empty, function names will be checked with the set +# naming style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Regular expression matching correct type alias names. If left empty, type +# alias names will be checked with the set naming style. +#typealias-rgx= + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. If left empty, variable names will be checked with the set +# naming style. +#variable-rgx= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + asyncSetUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=10 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=builtins.BaseException,builtins.Exception + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=200 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow explicit reexports by alias from a package __init__. +allow-reexport-from-package=no + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence=HIGH, + CONTROL_FLOW, + INFERENCE, + INFERENCE_FAILURE, + UNDEFINED + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then re-enable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=R0801, + missing-module-docstring, + missing-class-docstring, + missing-function-docstring, + too-many-public-methods, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + use-implicit-booleaness-not-comparison-to-string, + use-implicit-booleaness-not-comparison-to-zero + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable= + + +[METHOD_ARGS] + +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +notes-rgx= + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + +# Let 'consider-using-join' be raised when the separator to join on would be +# non-empty (resulting in expected fixes of the type: ``"- " + " - +# ".join(items)``) +suggest-join-with-non-empty-separator=yes + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each +# category, as well as 'statement' which is the total number of statements +# analyzed. This score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +msg-template= + +# Set the output format. Available formats are: text, parseable, colorized, +# json2 (improved json format), json (old json format) and msvs (visual +# studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +#output-format= + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=yes + +# Signatures are removed from the similarity computation +ignore-signatures=yes + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. No available dictionaries : You need to install +# both the python package and the system dependency for enchant to work. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins=no-member, + not-async-context-manager, + not-context-manager, + attribute-defined-outside-init + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io diff --git a/.vscode/.e2e.env b/.vscode/.e2e.env deleted file mode 100644 index 1a3ca42b..00000000 --- a/.vscode/.e2e.env +++ /dev/null @@ -1,2 +0,0 @@ -DEBUG=krux:* -NODE_ENV=test \ No newline at end of file diff --git a/.vscode/.script.mjs b/.vscode/.script.mjs deleted file mode 100644 index 8c418217..00000000 --- a/.vscode/.script.mjs +++ /dev/null @@ -1,32 +0,0 @@ -import fs from 'node:fs' -import path from 'node:path' -import { fileURLToPath } from 'node:url' -import { createRequire } from 'node:module' -import { spawn } from 'node:child_process' - -const pkg = createRequire(import.meta.url)('../package.json') -const __dirname = path.dirname(fileURLToPath(import.meta.url)) - -// parse .script.mjs option to create an -// appropriate .env file -const option = process.argv[2] - -// write .debug.env -const envContent = Object.entries(pkg.vscode[option].env).map(([key, val]) => `${key}=${val}`) -const envName = `.${option}.env` -const envPath = path.join(__dirname, envName) -console.log(`Creating ${envPath}`) -fs.writeFileSync(envPath, envContent.join('\n')) - -const runCommand = pkg.vscode[option].run.split(' ') - -// bootstrap -spawn( - // TODO: terminate `npm run dev` when Debug exits. - process.platform === 'win32' ? `${runCommand[0]}.cmd` : runCommand[0], - [runCommand[1], runCommand[2]], - { - stdio: 'inherit', - env: Object.assign(process.env, { VSCODE_DEBUG: 'true' }), - }, -) diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 232ead72..00000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "recommendations": [ - "Vue.volar", - "Vue.vscode-typescript-vue-plugin" - ] -} diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 7c1be395..00000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "compounds": [ - { - "name": "Debug App", - "preLaunchTask": "Before Debug", - "configurations": [ - "Debug Main Process", - "Debug Renderer Process" - ], - "presentation": { - "hidden": false, - "group": "", - "order": 1 - }, - "stopAll": true - }, - { - "name": "Test E2E App", - "preLaunchTask": "Before E2E", - "configurations": [ - "Test E2E init" - ], - "presentation": { - "hidden": false, - "group": "", - "order": 1 - }, - "stopAll": true - } - ], - "configurations": [ - { - "name": "Debug Main Process", - "type": "node", - "request": "launch", - "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", - "windows": { - "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" - }, - "runtimeArgs": [ - "--remote-debugging-port=9229", - "." - ], - "envFile": "${workspaceFolder}/.vscode/.debug.env", - "console": "integratedTerminal" - }, - { - "name": "Debug Renderer Process", - "port": 9229, - "request": "attach", - "type": "chrome", - "timeout": 60000, - "skipFiles": [ - "/**", - "${workspaceRoot}/node_modules/**", - "${workspaceRoot}/dist-electron/**", - // Skip files in host(VITE_DEV_SERVER_URL) - "http://127.0.0.1:3344/**" - ] - }, - { - "name": "Test E2E init", - "type": "node", - "request": "launch", - "runtimeExecutable": "yarn", - "windows": { - "runtimeExecutable": "yarn.cmd" - }, - "runtimeArgs": [ - "--remote-debugging-port=9229", - "run", - "e2e" - ], - "envFile": "${workspaceFolder}/.vscode/.e2e.env", - "console": "integratedTerminal" - } - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 1e3e2cde..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "typescript.tsdk": "node_modules/typescript/lib", - "typescript.tsc.autoDetect": "off", - "json.schemas": [ - { - "fileMatch": [ - "/*electron-builder.json5", - "/*electron-builder.json" - ], - "url": "https://json.schemastore.org/electron-builder" - } - ] -} diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 389bff2b..00000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - // See https://go.microsoft.com/fwlink/?LinkId=733558 - // for the documentation about the tasks.json format - "version": "2.0.0", - "tasks": [ - { - "label": "Before Debug", - "type": "shell", - "command": "node .vscode/.script.mjs debug", - "isBackground": true, - "problemMatcher": { - "owner": "typescript", - "fileLocation": "relative", - "pattern": { - // TODO: correct "regexp" - "regexp": "^([a-zA-Z]\\:\/?([\\w\\-]\/?)+\\.\\w+):(\\d+):(\\d+): (ERROR|WARNING)\\: (.*)$", - "file": 1, - "line": 3, - "column": 4, - "code": 5, - "message": 6 - }, - "background": { - "activeOnStart": true, - "beginsPattern": "^.*VITE v.* ready in \\d* ms.*$", - "endsPattern": "^.*\\[ startup \\] KruxInstaller v.*$" - } - } - }, - { - "label": "Before E2E", - "type": "shell", - "command": "node .vscode/.script.mjs e2e", - "isBackground": true, - "problemMatcher": { - "owner": "javascript", - "fileLocation": "relative", - "pattern": { - // TODO: correct "regexp" - "regexp": "^([a-zA-Z]\\:\/?([\\w\\-]\/?)+\\.\\w+):(\\d+):(\\d+): (ERROR|WARNING)\\: (.*)$", - "file": 1, - "line": 3, - "column": 4, - "code": 5, - "message": 6 - }, - "background": { - "activeOnStart": true, - "beginsPattern": "^Creating *.env$", - "endsPattern": "^Running E2E$" - } - } - } - ] -} diff --git a/CHANGELOG.md b/CHANGELOG.md index 0efb829c..0dea23d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,43 @@ -# 0.0.1 +# CHANGELOG + +## 0.0.2-alpha + +- code refactoration from `nodejs` to `python`; +- re-build project from `electron` to `kivy`; +- Support for MacOS (arm64 and intel processors); +- Support to download older versions; +- Support to devices according to the appropriate version: + - M5stickV; + - Amigo; + - Dock; + - Bit; + - Yahboom; + - Cube; +- Flash made with the `ktool` from its source; +- Wipe made with the `ktool` from its source; +- Added settings page: + - Enable change path of downloaded assets; + - Enable change of flash baudrate; + - Enable change of locale; +- Added about page +- Locale support for 10 languages: + - af_ZA (South Africa Afrikaans); + - en_US (USA English); + - es_ES (Spain spanish); + - fr_FR (France french); + - it_IT (Italian); + - ko_KR (South Korean korean); + - nl_NL (Netherlands dutch); + - pt_BR (Brazilian portuguese); + - ru_RU (Russian cyrillic); + - zh_CN (Simplified chinese) + +## 0.0.1 - Major updates dependencies: - `electron`: 28.1.0; - `vite-plugin-electron`: 0.15.5; - `wdio-electron-service`: 6.0.2. - - Minor updates: - `@wdio/cli`: 8.27.0; - `@wdio/globals`: 8.27.0; @@ -14,14 +47,9 @@ - `vue`: 3.3.13; - `vue-tsc`: 1.8.26; - `vuetify`: 3.4.8; - - Refactored `test/e2e/specs`: - to suit `wdio-electron-service` major updates that break E2E tests; - renamed extensions to `mts` to suit `vite-plugin-electron`; - updated krux firmware version checks to `23.09.1`; - - Updated `openssl` for windows to `3.2.0` *; - - Removed MacOS release since the current approach did not worked well on MacOS; - -> \* see [WARNING](WARNING.md) diff --git a/LICENSE b/LICENSE index 22edc0e9..c13f9911 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 草鞋没号 +Copyright (c) 2024 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 045ff520..3fa3cd85 100644 --- a/README.md +++ b/README.md @@ -1,181 +1,231 @@ # Krux Installer [![Build main branch](https://github.com/selfcustody/krux-installer/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/selfcustody/krux-installer/actions/workflows/build.yml) +[![codecov](https://codecov.io/gh/qlrd/krux-installer/tree/kivy/graph/badge.svg?token=KD41H20MYS)](https://codecov.io/gh/qlrd/krux-installer) +[![created at](https://img.shields.io/github/created-at/selfcustody/krux-installer)](https://github.com/selfcustody/krux-installer/commit/5d177795fe3df380c54d424ccfd0f23fc7e62c41) +[![downloads](https://img.shields.io/github/downloads/selfcustody/krux-installer/total)](https://github.com/selfcustody/krux-installer/releases) +[![downloads (latest release)](https://img.shields.io/github/downloads/selfcustody/krux-installer/latest/total)](https://github.com/selfcustody/krux-installer/releases) +[![commits (since latest release)](https://img.shields.io/github/commits-since/selfcustody/krux-installer/latest/main)](https://github.com/qlrd/krux-installer/compare/main...kivy) + +Krux Installer is a GUI based tool to flash [Krux](https://github.com/selfcustody/krux) +without typing any command in terminal for [flash the firmware onto the device](https://selfcustody.github.io/krux/getting-started/installing/#flash-the-firmware-onto-the-device). + +## Installing + +There are pre-built +[releases](https://github.com/selfcustody/krux-installer/releases) for: + +* Linux: + * Debian-like + * Fedora-like +* Windows +* MacOS: + * intel processors + * arm64 processors (M1/M2/M3) + +To build it from the source, please follow the steps below: + +* [System setup](/#system-setup) + * [Linux](/#linux) + * [Windows](/#windows) + * [MacOS](/#macos) + * [Install brew package manager](/#install-brew-package-manager) + * [Install latest python](/#install-latest-python) + * [Ensure openssl have a correct link](/#ensure-openssl-have-a-correct-link) + * [Patch your zshrc](/#patch-your-zshrc) + * [Install poetry](/#install-poetry) +* [Download sources](/#download-sources) +* [Update code](/#update-code) +* [Developing](/#developing) + * [Format code](/#format-code) + * [Lint](/#lint) + * [Test](/#test) + * [Build](/#build) + +## System setup + +Make sure you have python: -Krux Installer (alpha versions) aims to be a GUI based tool to build, -flash and debug [Krux](https://github.com/selfcustody/krux) - -As it now, the generated application execute, -without typing any command in terminal. +```bash +python --version +``` -For more information, see [flash the firmware onto the device](https://selfcustody.github.io/krux/getting-started/installing/#flash-the-firmware-onto-the-device). +### Linux -## Tested machines +Generally, all Linux come with python. -- Linux: - - Archlinux; - - Ubuntu; -- Windows: - - Windows 10 +### Windows -## Untested machines +Follow the instructions at [python.org](https://www.python.org/downloads/windows/) -- MacOS +### MacOS -## Install +Before installing `krux-installer` source code, you will need prepare the system: -- See [releases page](https://github.com/selfcustody/krux-installer/releases); -or +#### Install `brew` package manager -- [Build from source](/#build-from-source) +```bash +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +``` -## Build from source +#### Install latest `python` -### Download nodejs +```bash +brew install python +``` -First of all, you will need [Node.js](https://nodejs.org) -installed in you system. We recommend use the latest LTS version. +and add this line to your `~/.zshrc`: -#### Download from Node.js binaries +```bash +alias python=python3 +``` -You can install node.js in your system downloading it from official -[nodejs website](https://nodejs.org/en/download) and following -provided instructions. +#### Ensure `openssl` have a correct link -#### Download from NVM (Node Version Manager) +Python's `ssl` module relies on OpenSSL for cryptographic operations. +Ensure that OpenSSL is installed on your system and is compatible with the +Python version you're using. -Alternatively, if you have a linux or macos system, -you can have multiple versions of Node.js using [nvm](https://github.com/nvm-sh/nvm). +Since we expect that you're using the Python installed with Homebrew, +it's recommended to install OpenSSL through Homebrew if it's not already +installed: -To install nvm, -follow the [instructions](https://github.com/nvm-sh/nvm#installing-and-updating). +```bash +brew install openssl +``` -Once installed, -we recomend to install the latest LTS node: +After installing OpenSSL, make sure it's linked correctly: ```bash -nvm install --lts +brew link --force openssl ``` -### Download repository +This ensures that the OpenSSL libraries are available in the expected +locations that Python can find and use. -Now you can download the source code: +#### Patch your zshrc -```bash -git clone https://github.com/qlrd/krux-installer.git -``` +Library paths on MacOS involves verifying that the environment variables and system +configurationsare correctyly set to find the necessary libraries, such as OpenSSL, +which is crucial for the `ssl` module in Python. -### Install dependencies +On MacOS, the dynamic linker tool `dyld` uses environment variabes to locate shared +libraries. The primary environment variable for specifying library paths is +`DYLD_LIBRARY_PATH`. -Install all dependencies: +Adding the lines below to your `~/.zshrc` (or similar) the `DYLD_LIBRARY_PATH` +will be set each time you open a new terminal session (and therefore the OpenSSL +libraries `libcrypto.dylib` and `libssl.dylib` will can be found): ```bash -yarn install +OPENSSL_MAJOR_VERSION=`openssl --version | awk '{ print $2}' | cut -d . -f1` +OPENSSL_FULL_VERSION=`openssl --version | awk ' { print $2}'` +export DYLD_LIBRARY_PATH="/opt/homebrew/Cellar/openssl@$OPENSSL_MAJOR_VERSION/$OPENSSL_FULL_VERSION/lib:$DYLD_LIBRARY_PATH" ``` -Additionaly, you can upgrade dependencies to its latest versions. -Have some caution with this command, once that executing this command -can broke some functionalities, mainly those related to the use of -`google-chrome` and `chromiumdriver` in E2e tests. +### Install poetry -**TIP**: Before execute this command, always check the latest supported -`chromium` version at -[Electron Stable Releases page](https://releases.electronjs.org/releases/stable) +Make sure you have `poetry` installed: -```bash -yarn upgrade-interactive --latest -``` +```b̀ash +python -m pipx install poetry +```` + +If you have problems with installation, make sure to +properly [configure its options](https://pipx.pypa.io/latest/installation/#installation-options). -### Live compile to development environment +## Download sources -When a change is made, we recommend to execute `dev` subcommand: +Clone the repository: ```bash -yarn run dev +git clone --recurse-submodules https://github.com/krux-installer.git ``` -if you want to show some debug messages: +Install python dependencies: -```bash -DEBUG=krux:* yarn run dev +```b̀ash +poetry install ``` -#### Debug development app with VSCode/VSCodium +## Update code -If you're codding with VSCode/VSCodium, go to `Run and Debug` -tab and select `Debug App`: +If already cloned the repo without using `--recurse-submodules`, +use the command below to clone the needed submodules: -![VScodium Debug](images/vscodium_debug.png) +```bash +git submodule update --init +``` -### Test +## Developing -#### Prepare tests +Krux-Installer uses `poe` task manager for formatting, linting, +tests and coverage. To see all available tasks, run: -To test, -you need to write `specification` tests under `pageobjects` definitions: +```bash +poetry run poe +``` -- You can write your own [E2E](https://webdriver.io) -specification test files on `test/e2e/specs` folder; +### Format code -- You can define the [PageObjects] on -`test/e2e/pageobjects` folder. +```bash +poetry run poe format +``` -Before run tests, -you will need to **build** the application. +### Lint -#### Build +```bash +poetry run poe lint +``` -Before running build, -verify [builder config](electron-builder.json5) -to setup the build `target` on specific `os` (Operational System). +### Test -The `` depends depends on the running platform -(i.e., `linux`, `darwin` -- MacOS, and `win32` -- Windows). +```bash +poetry run poe test +``` -For more information, -see [Electron Builder](https://www.electron.build/configuration/configuration) -page. +For systems without a window manager: -#### Run all tests +```bash +poetry run poe test --no-xvfb +``` -The `wdio.conf.mts` is configured to check -if your system have `krux.zip.*` resources. +### Build -- If not, it will, run all tests, including download tests; -- If yes, it will skip tests that download resources. +At the moment, you'll need to [patch some code on `kivy`](https://github.com/kivy/kivy/issues/8653#issuecomment-2028509695) +to build the Graphical User Interface: -```bash -yarn run build -``` +#### Build for Debian, Fedora, MacOS -If you want to build a specific `target` -to a specifi `os`, run +Make sure you have the `wget` tool to download a +[specific commit](https://raw.githubusercontent.com/ikus060/kivy/21c7110ee79f355d6a42da0a274d2426b1e18665/kivy/tools/packaging/pyinstaller_hooks/__init__.py). -```bash -yarn run build -- -``` +If you not have: -If you want to debug some messages, add the -`DEBUG` environment variable. +* Debian: `sudo apt-get install wget`; +* Fedora: `sudo dnf install wget`; +* MacOS: `brew install wget`. -In linux/mac: +Then you can patch PyInstaller hook for kivy and build an executable: ```bash -DEBUG=krux:* yarn run build -- +poetry run poe patch-nix +poetry run poe build-nix ``` -##### Run tests - -To run all tests in command line: +#### Build for Windows ```bash -NODE_ENV=test yarn run e2e +poetry run poe patch-win +poetry run poe build-win ``` -#### Debug test in VSCode/VSCodium +It will export all project in a +[`one-file`](https://pyinstaller.org/en/stable/usage.html#cmdoption-F) binary: -If you're codding with VSCode/VSCodium, the `NODE_ENV` -variable is already configured. To run, tests, go to `Run and Debug` -tab and select `Test E2E App`: +* linux: `./dist/krux-installer` +* macOS: `./dist/krux-installer.app/Contents/MacOS/krux-installer` +* windows: `./dist/krux-installer.exe` -![VScodium E2E test](images/vscodium.png) +To more options see [.ci/create-spec.py](./.ci/create-spec.py) +against the PyInstaller [options](https://pyinstaller.org). diff --git a/TODO.md b/TODO.md index aab660fe..c3a494c2 100644 --- a/TODO.md +++ b/TODO.md @@ -5,29 +5,17 @@ - [x] Flash to Sipeed Amigo; - [x] Flash to Sipeed Bit; - [x] Flash to Sipeed Dock; - - [ ] Flash to Yahboom Aimotion - - [ ] Build from source to M5stickV; - - [ ] Build from source to Sipeed Amigo; - - [ ] Build from source to Sipeed Bit; - - [ ] Build from source to Sipeed Dock; - - [ ] Debug for M5stickV; - - [ ] Debug for Sipeed Amigo; - - [ ] Debug for Sipeed Bit; - - [ ] Debug for Sipeed Dock; + - [x] Flash to Yahboom Aimotion + - [x] Flash to Sipeed Cube + - [x] Flash to Yahboom WonderMV - [odudex Android version](https://github.com/odudex/krux_binaries/tree/main/Android): - [ ] Transfer to device; - [ ] Build for device; - [ ] Debug for device. - Windows: - [x] Build NSIS installer; - - [ ] Build Portable installer; - - [ ] Build AppX installer; - Linux: - - [x] Build `AppImage` standalone; - [x] Build `deb` package for [apt-get](https://www.debian.org/doc/manuals/apt-howto/); - - [ ] Build `snap` package for [snapcraft](https://snapcraft.io/); - - [ ] Build `pacman` package for [pacman](https://wiki.archlinux.org/title/Pacman). + - [x] Build `rpm` package for [apt-get](https://access.redhat.com/sites/default/files/attachments/rpm_building_howto.pdf); - MacOS: - [x] Build DMG installer; - - [ ] Build PKG installer; - - [ ] Build MAS installer; diff --git a/WARNING.md b/WARNING.md deleted file mode 100644 index 128ebc96..00000000 --- a/WARNING.md +++ /dev/null @@ -1,132 +0,0 @@ -# Warning - -We will replace the development of `krux-installer` from `typescript/electron` -to `python/kivy`. - -## Why the change? - -The decision was made among the members of the `selfcustody` -team for the following reasons: - -- To unify software development in Python; -- Behaviors with SSL routines, in windows, - that differ from what would be considered "normalized"; -- Failure to flash in MacOS. - -### Why unify software development in Python - -Maintenance and review of code can be more extensive. - -### What systems differ in behavior with SSL routines? - -- Windows -- MacOS - -### Why we need SSL routines? - -When downloading official krux firmware versions, it is necessary to verify -the signature through an external OpenSSL tool, as a way to verify the authenticity -of the downloaded binaries. - -## Why behaviors with SSL routines differ? - -We need to pack an external `openssl` into `krux-installer` package. - -### Why? - -On "Unix like" releases (Linux and MacOS), verification is easily done -since such tool exists natively in the operating system. -In windows release, we are faced with the peculiarity of the operating -system in question. Windows does not natively have such a tool -(see this [issue](https://github.com/qlrd/krux-installer/issues/2)). - -### How has it been resolved so far? - -We compiled a stable version of OpenSSL from the -[source](https://github.com/openssl/openssl) and packaged it on our software. - -#### This isn't insecure? - -We believe not, since the compilation process -is done entirely in a reproducible virtual environment and, -therefore, not locally, with the github-action -[compile-openssl-windows-action](https://github.com/qlrd/compile-openssl-windows-action/actions). - -## MacOS: how flash fails? - -The application works until you try to flash the device. Once you try -to flash, a message like this will appear: - -```bash -Error: 0:336: execution error: [1047] Cannot open PyInstaller -archive from executable -(/Users/user/Documents/krux-installer/krux-v23.09.0/ktool-mac) -or external archive -(/Users/user/Documents/krux-installer/krux-v23.09.0/ktool-mac.pkg) (255) - - at Socket. (/Applications/krux-installer.app/Contents/Resources/app.asar/dist-electron/main/index.js:6:381) - at Socket.emit (node:events:513:28) - at addChunk (node:internal/streams/readable:324:12) - at readableAddChunk (node:internal/streams/readable:297:9) - at Socket.push (node:internal/streams/readable:234:10) - at Pipe.onStreamRead (node:internal/stream_base_commons:190:23) -``` - -### What this means? - -This means that the `ktool-mac`, aka the "flasher", -in `krux-installer` failed to be executed. - -### Why this happen? - -Although we don't know for sure, we suspect that: - -- (1) The `krux-installer` download `ktool-mac` from a source -that `apple` did not recognize as "safe"; -- (2) If it isn't "safe", `macOS` adds an [extended file permission](https://apple.stackexchange.com/questions/42177/what-does-signify-in-unix-file-permissions); -- (3) This extended file permission puts the `ktool-mac` in a quarantine; -- (4) flash will not work. - -## And why not use a nodejs module, instead an external tool, to verify? - -The usage of SSL routines, in nodejs is done by -[Native Node Modules](https://www.electronjs.org/docs/latest/tutorial/using-native-node-modules). - -### And? - -As stated by `electron` documentation: - -> Native Node.js modules are supported by Electron, -but since Electron has a different application binary interface (ABI) -from a given Node.js binary (due to **differences such as using Chromium's -BoringSSL instead of OpenSSL**) - -### What's BoringSSL? - -As stated by BoringSSL [`README`](https://github.com/google/boringssl): - -> BoringSSL is a fork of OpenSSL that is designed to meet Google's needs. - -### How BoringSSL affects krux-installer? - -The [curve used](https://github.com/selfcustody/krux/blob/7add64a0fa8cdae65e49f8bd9bd0f7ff09e95e84/krux#L151) -to sign the firmware is `secp256k1` (the same used in Bitcoin). - -The BoringSSL [does not implement -`secp256k1`](https://github.com/electron/electron/issues/32535) and, -therefore, it is not possible -to check this curve using javascript code in electron. - -## These can be solved with a python module? - -Yes, we believe so; - -### Which module? - -[pyOpenSSL](https://pypi.org/project/pyOpenSSL/); - -### And why do you believe that? - -We've already made an experiment with -[`krux-file-signer`](https://github.com/selfcustody/krux-file-signer/blob/c541dbc730f833d64c068245fae30a42bc3f2580/src/cli/verifyer.py#L97) -in linux and windows. diff --git a/assets/NotoSansCJK_Cy_SC_KR_Krux.ttf b/assets/NotoSansCJK_Cy_SC_KR_Krux.ttf new file mode 100644 index 00000000..a9cb8df9 Binary files /dev/null and b/assets/NotoSansCJK_Cy_SC_KR_Krux.ttf differ diff --git a/assets/done.png b/assets/done.png new file mode 100644 index 00000000..f67f220e Binary files /dev/null and b/assets/done.png differ diff --git a/assets/error.png b/assets/error.png new file mode 100644 index 00000000..56ef8066 Binary files /dev/null and b/assets/error.png differ diff --git a/assets/head.bmp b/assets/head.bmp new file mode 100644 index 00000000..fd82a493 Binary files /dev/null and b/assets/head.bmp differ diff --git a/assets/icon.icns b/assets/icon.icns new file mode 100644 index 00000000..6a676884 Binary files /dev/null and b/assets/icon.icns differ diff --git a/assets/icon.ico b/assets/icon.ico new file mode 100644 index 00000000..c10eb05b Binary files /dev/null and b/assets/icon.ico differ diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 00000000..b68fe2b1 Binary files /dev/null and b/assets/icon.png differ diff --git a/assets/load.gif b/assets/load.gif new file mode 100644 index 00000000..2929f58b Binary files /dev/null and b/assets/load.gif differ diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 00000000..1976f34c Binary files /dev/null and b/assets/logo.png differ diff --git a/assets/warning.png b/assets/warning.png new file mode 100644 index 00000000..06c688d5 Binary files /dev/null and b/assets/warning.png differ diff --git a/assets/welcome.bmp b/assets/welcome.bmp new file mode 100644 index 00000000..36c3daa2 Binary files /dev/null and b/assets/welcome.bmp differ diff --git a/dockerfiles/deb/Dockerfile b/dockerfiles/deb/Dockerfile deleted file mode 100644 index b9b0caef..00000000 --- a/dockerfiles/deb/Dockerfile +++ /dev/null @@ -1,28 +0,0 @@ -FROM arm64v8/node AS build-stage -ENV NODE_ENV "test" -ENV NODE_DOCKER true -ENV DEBUG "krux:*" -ENV USE_SYSTEM_FPM true -ENV DOCUMENTS /app/Documents - -ADD . /app -WORKDIR /app -RUN apt-get update && \ - apt-get upgrade -y && \ - apt-get install -y \ - libopenjp2-tools \ - ruby \ - ruby-dev - -RUN gem install fpm -RUN yarn install -RUN yarn run build --arm64 --linux deb -#RUN apt-get install --fix-missing -y xvfb -#RUN yarn add --dev xvfb-maybe -#RUN node ./node_modules/.bin/xvfb-maybe \ -# ./node_modules/.bin/wdio \ -# run \ -# wdio.conf.mts - -FROM scratch AS export-stage -COPY --from=build-stage /app/release / diff --git a/dockerfiles/rpm/Dockerfile b/dockerfiles/rpm/Dockerfile deleted file mode 100644 index 422debc0..00000000 --- a/dockerfiles/rpm/Dockerfile +++ /dev/null @@ -1,29 +0,0 @@ -FROM arm64v8/node AS build-stage -ENV NODE_ENV "test" -ENV NODE_DOCKER true -ENV DEBUG "krux:*" -ENV USE_SYSTEM_FPM true -ENV DOCUMENTS /app/Documents - -ADD . /app -WORKDIR /app -RUN apt-get update && \ - apt-get upgrade -y && \ - apt-get install -y \ - libopenjp2-tools \ - ruby \ - ruby-dev \ - rpm - -RUN gem install fpm -RUN yarn install -RUN yarn run build --arm64 --linux rpm -#RUN apt-get install --fix-missing -y xvfb -#RUN yarn add --dev xvfb-maybe -#RUN node ./node_modules/.bin/xvfb-maybe \ -# ./node_modules/.bin/wdio \ -# run \ -# wdio.conf.mts - -FROM scratch AS export-stage -COPY --from=build-stage /app/release / diff --git a/e2e/__init__.py b/e2e/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/e2e/test_000_base_screen.py b/e2e/test_000_base_screen.py new file mode 100644 index 00000000..6fa8dcde --- /dev/null +++ b/e2e/test_000_base_screen.py @@ -0,0 +1,234 @@ +import os +from pathlib import Path +from unittest.mock import patch, call, MagicMock +from kivy.base import EventLoop, EventLoopBase +from kivy.tests.common import GraphicUnitTest +from kivy.uix.button import Button +from kivy.uix.screenmanager import ScreenManager +from src.app.screens.base_screen import BaseScreen + + +class TestBaseScreen(GraphicUnitTest): + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.BaseScreen.get_locale") + def test_render(self, mock_get_locale): + + screen = BaseScreen(wid="mock", name="Mock") + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + # your asserts + self.assertEqual(window.children[0], screen) + self.assertEqual(window.children[0].height, window.height) + + mock_get_locale.assert_called_once() + + @patch("src.app.screens.base_screen.BaseScreen.get_locale") + def test_get_logo_img(self, mock_get_locale): + root = Path(__file__).parent.parent + logo = os.path.join(root, "assets", "logo.png") + screen = BaseScreen(wid="mock", name="Mock") + self.assertEqual(screen.logo_img, logo) + + mock_get_locale.assert_called_once() + + @patch("src.app.screens.base_screen.BaseScreen.get_locale") + def test_get_warn_img(self, mock_get_locale): + root = Path(__file__).parent.parent + warn = os.path.join(root, "assets", "warning.png") + screen = BaseScreen(wid="mock", name="Mock") + self.assertEqual(screen.warn_img, warn) + + mock_get_locale.assert_called_once() + + @patch("src.app.screens.base_screen.BaseScreen.get_locale") + def test_get_load_gif(self, mock_get_locale): + root = Path(__file__).parent.parent + warn = os.path.join(root, "assets", "load.gif") + screen = BaseScreen(wid="mock", name="Mock") + self.assertEqual(screen.load_img, warn) + + mock_get_locale.assert_called_once() + + @patch("src.app.screens.base_screen.BaseScreen.get_locale") + def test_get_done_img(self, mock_get_locale): + root = Path(__file__).parent.parent + warn = os.path.join(root, "assets", "done.png") + screen = BaseScreen(wid="mock", name="Mock") + self.assertEqual(screen.done_img, warn) + + mock_get_locale.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.BaseScreen.get_locale") + def test_set_background(self, mock_get_locale): + screen = BaseScreen(wid="mock", name="Mock") + screen.ids = {} + screen.ids["mocked_button"] = Button() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + screen.set_background(wid="mocked_button", rgba=(0, 0, 0, 0)) + + # your asserts + self.assertEqual(window.children[0].ids["mocked_button"].background_color[0], 0) + self.assertEqual(window.children[0].ids["mocked_button"].background_color[1], 0) + self.assertEqual(window.children[0].ids["mocked_button"].background_color[2], 0) + self.assertEqual(window.children[0].ids["mocked_button"].background_color[3], 0) + + mock_get_locale.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.BaseScreen.get_locale") + def test_set_screen(self, mock_get_locale): + screen_manager = ScreenManager() + screen_0 = BaseScreen(wid="mock_0", name="Mock_0") + screen_1 = BaseScreen(wid="mock_1", name="Mock_1") + + screen_manager.add_widget(screen_0) + screen_manager.add_widget(screen_1) + self.render(screen_manager) + + self.assertEqual(screen_manager.current, "Mock_0") + + screen_0.set_screen(name="Mock_1", direction="left") + self.assertEqual(screen_manager.current, "Mock_1") + + screen_1.set_screen(name="Mock_0", direction="right") + self.assertEqual(screen_manager.current, "Mock_0") + + mock_get_locale.assert_has_calls([call(), call()], any_order=True) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.BaseScreen.get_locale") + def test_make_grid(self, mock_get_locale): + screen_0 = BaseScreen(wid="mock", name="Mock") + screen_0.make_grid(wid="mock_grid", rows=1) + self.render(screen_0) + + EventLoop.ensure_window() + window = EventLoop.window + screen = window.children[0] + + self.assertTrue("mock_grid" in screen.ids) + self.assertEqual(screen.id, "mock") + self.assertEqual(screen.name, "Mock") + + mock_get_locale.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.BaseScreen.get_locale") + def test_make_subgrid(self, mock_get_locale): + screen = BaseScreen(wid="mock", name="Mock") + screen.make_grid(wid="mock_grid", rows=1) + screen.make_subgrid(wid="mock_subgrid", rows=1, root_widget="mock_grid") + self.render(screen) + + EventLoop.ensure_window() + + self.assertTrue("mock_grid" in screen.ids) + self.assertTrue("mock_subgrid" in screen.ids) + self.assertEqual(screen.id, "mock") + self.assertEqual(screen.name, "Mock") + + mock_get_locale.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.BaseScreen.get_locale") + def test_make_label(self, mock_get_locale): + screen = BaseScreen(wid="mock", name="Mock") + screen.make_grid(wid="mock_grid", rows=1) + screen.make_label( + wid="mock_label", + root_widget="mock_grid", + halign="center", + text="mock", + markup=False, + ) + self.render(screen) + + EventLoop.ensure_window() + + self.assertTrue("mock_grid" in screen.ids) + self.assertTrue("mock_label" in screen.ids) + self.assertEqual(screen.ids["mock_label"].text, "mock") + self.assertEqual(screen.id, "mock") + self.assertEqual(screen.name, "Mock") + + mock_get_locale.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.BaseScreen.get_locale") + def test_make_button(self, mock_get_locale): + screen_0 = BaseScreen(wid="mock", name="Mock") + screen_0.make_grid(wid="mock_grid", rows=1) + screen_0.make_button( + row=0, + id="mock_button", + root_widget="mock_grid", + text="Mocked button", + markup=False, + on_press=MagicMock(), + on_release=MagicMock(), + ) + self.render(screen_0) + + EventLoop.ensure_window() + window = EventLoop.window + screen = window.children[0] + + self.assertTrue("mock_button" in screen.ids) + + button = screen.ids["mock_button"] + + # pylint: disable=protected-access + button._do_press() + button.dispatch("on_press") + + # pylint: disable=protected-access + button._do_release() + button.dispatch("on_release") + + screen.on_press_mock_button.assert_called_once() + screen.on_release_mock_button.assert_called_once() + + mock_get_locale.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.BaseScreen.get_locale") + def test_clear_grid(self, mock_get_locale): + screen_0 = BaseScreen(wid="mock", name="Mock") + screen_0.make_grid(wid="mock_grid", rows=3) + + for i in range(0, 2): + screen_0.make_button( + row=i, + id=f"mock_button_{i}", + root_widget="mock_grid", + text="Mocked button", + markup=False, + on_press=MagicMock(), + on_release=MagicMock(), + ) + self.render(screen_0) + + EventLoop.ensure_window() + window = EventLoop.window + screen = window.children[0] + grid = screen.children[0] + + screen.clear_grid(wid="mock_grid") + self.assertFalse("mock_button_0" in grid.ids) + + mock_get_locale.assert_called_once() diff --git a/e2e/test_001_greetings_screen.py b/e2e/test_001_greetings_screen.py new file mode 100644 index 00000000..5e482b1e --- /dev/null +++ b/e2e/test_001_greetings_screen.py @@ -0,0 +1,321 @@ +import os +import sys +from pathlib import Path +from unittest.mock import patch, MagicMock +from kivy.base import EventLoop, EventLoopBase +from kivy.tests.common import GraphicUnitTest +from src.app.screens.greetings_screen import GreetingsScreen + + +class TestAboutScreen(GraphicUnitTest): + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.App.get_running_app") + def test_render_main_screen_only_logo(self, mock_get_running_app): + mock_get_running_app.config = MagicMock() + mock_get_running_app.config.get = MagicMock(return_value="en-US") + + screen = GreetingsScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + image = grid.children[0] + + # default assertions + root = Path(__file__).parent.parent + assets = os.path.join(root, "assets", "logo.png") + self.assertEqual(window.children[0], screen) + self.assertEqual(screen.name, "GreetingsScreen") + self.assertEqual(screen.id, "greetings_screen") + self.assertEqual(image.source, assets) + + # patch assertions + mock_get_running_app.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.App.get_running_app") + def test_update_fail_invalid_name(self, mock_get_running_app): + mock_get_running_app.config = MagicMock() + mock_get_running_app.config.get = MagicMock(return_value="en-US") + + screen = GreetingsScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # default assertions + with self.assertRaises(ValueError) as exc_info: + screen.update(name="Mock") + + self.assertEqual(str(exc_info.exception), "Invalid screen: Mock") + + # patch assertions + mock_get_running_app.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.App.get_running_app") + def test_update_fail_invalid_key(self, mock_get_running_app): + mock_get_running_app.config = MagicMock() + mock_get_running_app.config.get = MagicMock(return_value="en-US") + + screen = GreetingsScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # Do the tests + with self.assertRaises(ValueError) as exc_info: + screen.update(name="GreetingsScreen", key="mock") + + # default assertions + self.assertEqual(str(exc_info.exception), "Invalid key: 'mock'") + + # patch assertions + mock_get_running_app.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.App.get_running_app") + def test_update_fail_invalid_value_none(self, mock_get_running_app): + mock_get_running_app.config = MagicMock() + mock_get_running_app.config.get = MagicMock(return_value="en-US") + + screen = GreetingsScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # Do the tests + with self.assertRaises(ValueError) as exc_info: + screen.update(name="GreetingsScreen", key="change_screen", value=None) + + # default assertions + self.assertEqual( + str(exc_info.exception), "Invalid value for 'change_screen': None" + ) + + # patch assertions + mock_get_running_app.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.App.get_running_app") + def test_update_fail_invalid_value(self, mock_get_running_app): + mock_get_running_app.config = MagicMock() + mock_get_running_app.config.get = MagicMock(return_value="en-US") + + screen = GreetingsScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # Do the tests + with self.assertRaises(ValueError) as exc_info: + screen.update(name="GreetingsScreen", key="change_screen", value="mock") + + # default assertions + self.assertEqual( + str(exc_info.exception), "Invalid value for 'change_screen': mock" + ) + + # patch assertions + mock_get_running_app.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.App.get_running_app") + @patch("src.app.screens.greetings_screen.GreetingsScreen.set_screen") + def test_update_change_screen_to_main_screen( + self, mock_set_screen, mock_get_running_app + ): + mock_get_running_app.config = MagicMock() + mock_get_running_app.config.get = MagicMock(return_value="en-US") + + screen = GreetingsScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # Do the tests + screen.update(name="GreetingsScreen", key="change_screen", value="MainScreen") + + # patch assertions + mock_get_running_app.assert_called_once() + mock_set_screen.assert_called_once_with(name="MainScreen", direction="left") + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.App.get_running_app") + @patch("src.app.screens.greetings_screen.GreetingsScreen.set_screen") + def test_update_change_screen_to_check_permissions_screen( + self, mock_set_screen, mock_get_running_app + ): + mock_get_running_app.config = MagicMock() + mock_get_running_app.config.get = MagicMock(return_value="en-US") + + screen = GreetingsScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # Do the tests + screen.update( + name="GreetingsScreen", key="change_screen", value="CheckPermissionsScreen" + ) + + # patch assertions + mock_get_running_app.assert_called_once() + mock_set_screen.assert_called_once_with( + name="CheckPermissionsScreen", direction="left" + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.App.get_running_app") + @patch("src.app.screens.greetings_screen.Color") + @patch("src.app.screens.greetings_screen.Rectangle") + def test_update_canvas(self, mock_rectangle, mock_color, mock_get_running_app): + mock_get_running_app.config = MagicMock() + mock_get_running_app.config.get = MagicMock(return_value="en-US") + + screen = GreetingsScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # Do the tests + screen.update(name="GreetingsScreen", key="canvas") + + # patch assertions + mock_get_running_app.assert_called_once() + mock_color.assert_called_once_with(0, 0, 0) + + if sys.platform == ("linux", "win32"): + mock_rectangle.assert_called_once_with(pos=(0, 0), size=(800, 600)) + elif sys.platform == "darwin": + mock_rectangle.assert_called_once_with(pos=(0, 0), size=(1600, 1200)) + + @patch("sys.platform", "linux") + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.App.get_running_app") + @patch("src.app.screens.greetings_screen.partial") + @patch("src.app.screens.greetings_screen.Clock.schedule_once") + def test_update_check_permissions_linux( + self, mock_schedule_once, mock_partial, mock_get_running_app + ): + mock_get_running_app.config = MagicMock() + mock_get_running_app.config.get = MagicMock(return_value="en-US") + + screen = GreetingsScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # Do the tests + screen.update(name="GreetingsScreen", key="check_permissions") + + # patch assertions + mock_get_running_app.assert_called_once() + mock_partial.assert_called_once_with( + screen.update, + name=screen.name, + key="change_screen", + value="CheckPermissionsScreen", + ) + mock_schedule_once.assert_called_once_with(mock_partial(), 2.1) + + @patch("sys.platform", "darwin") + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.App.get_running_app") + @patch("src.app.screens.greetings_screen.partial") + @patch("src.app.screens.greetings_screen.Clock.schedule_once") + def test_update_check_permissions_darwin( + self, mock_schedule_once, mock_partial, mock_get_running_app + ): + mock_get_running_app.config = MagicMock() + mock_get_running_app.config.get = MagicMock(return_value="en-US") + + screen = GreetingsScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # Do the tests + screen.update(name="GreetingsScreen", key="check_permissions") + + # patch assertions + mock_get_running_app.assert_called_once() + mock_partial.assert_called_once_with( + screen.update, + name=screen.name, + key="change_screen", + value="CheckInternetConnectionScreen", + ) + mock_schedule_once.assert_called_once_with(mock_partial(), 2.1) + + @patch("sys.platform", "win32") + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.App.get_running_app") + @patch("src.app.screens.greetings_screen.partial") + @patch("src.app.screens.greetings_screen.Clock.schedule_once") + def test_update_check_permissions_win32( + self, mock_schedule_once, mock_partial, mock_get_running_app + ): + mock_get_running_app.config = MagicMock() + mock_get_running_app.config.get = MagicMock(return_value="en-US") + + screen = GreetingsScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # Do the tests + screen.update(name="GreetingsScreen", key="check_permissions") + + # patch assertions + mock_get_running_app.assert_called_once() + mock_partial.assert_called_once_with( + screen.update, + name=screen.name, + key="change_screen", + value="CheckInternetConnectionScreen", + ) + mock_schedule_once.assert_called_once_with(mock_partial(), 2.1) + + @patch("sys.platform", "mockos") + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.App.get_running_app") + @patch("src.app.screens.greetings_screen.partial") + @patch("src.app.screens.greetings_screen.Clock.schedule_once") + def test_fail_update_check_permissions_wrong_os( + self, mock_schedule_once, mock_partial, mock_get_running_app + ): + mock_get_running_app.config = MagicMock() + mock_get_running_app.config.get = MagicMock(return_value="en-US") + + # get your Window instance safely + EventLoop.ensure_window() + + # Do the tests + with self.assertRaises(RuntimeError) as exc_info: + GreetingsScreen() + + # default assertions + self.assertEqual(str(exc_info.exception), "Not implemented for 'mockos'") + + # patch assertions + mock_get_running_app.assert_called_once() + mock_partial.assert_not_called() + mock_schedule_once.assert_not_called() diff --git a/e2e/test_002_check_permissions_screen.py b/e2e/test_002_check_permissions_screen.py new file mode 100644 index 00000000..780076d1 --- /dev/null +++ b/e2e/test_002_check_permissions_screen.py @@ -0,0 +1,685 @@ +import os +import sys +from unittest.mock import patch, MagicMock, call +from pytest import mark +from kivy.base import EventLoop, EventLoopBase +from kivy.tests.common import GraphicUnitTest +from kivy.core.text import LabelBase, DEFAULT_FONT +from src.app.screens.check_permissions_screen import CheckPermissionsScreen + + +# WARNING: Do not run these tests on windows +# they will break because it do not have the builtin 'grp' module +@mark.skipif(sys.platform == "win32", reason="does not run on windows") +class TestCheckPermissionsScreen(GraphicUnitTest): + + @classmethod + def setUpClass(cls): + cwd_path = os.path.dirname(__file__) + rel_assets_path = os.path.join(cwd_path, "..", "assets") + assets_path = os.path.abspath(rel_assets_path) + noto_sans_path = os.path.join(assets_path, "NotoSansCJK_Cy_SC_KR_Krux.ttf") + LabelBase.register(DEFAULT_FONT, noto_sans_path) + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.BaseScreen.get_locale") + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_update_fail_invalid_name(self, mock_redirect_error, mock_get_locale): + screen = CheckPermissionsScreen() + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + screen.update(name="MockScreen") + + # patch assertions + mock_get_locale.assert_called_once() + mock_redirect_error.assert_called_once_with(msg="Invalid screen: 'MockScreen'") + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.BaseScreen.get_locale") + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_update_fail_invalid_key(self, mock_redirect_error, mock_get_locale): + screen = CheckPermissionsScreen() + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + screen.update(name="ConfigKruxInstaller", key="mock") + + # patch assertions + mock_get_locale.assert_called_once() + mock_redirect_error.assert_called_once_with(msg="Invalid key: 'mock'") + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("sys.platform", "linux") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + @patch( + "src.app.screens.check_permissions_screen.os.environ.get", + return_value="mockeduser", + ) + @patch("src.app.screens.check_permissions_screen.distro.id", return_value="mockos") + @patch( + "src.app.screens.check_permissions_screen.distro.like", return_value="mockos" + ) + @patch( + "src.app.screens.check_permissions_screen.distro.name", return_value="mockos" + ) + def test_update_fail_check_user( + self, + mock_name, + mock_like, + mock_id, + mock_environ_get, + mock_redirect_error, + mock_get_locale, + ): + screen = CheckPermissionsScreen() + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + screen.user = "mockeduser" + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + screen.update(name="CheckPermissionsScreen", key="check_user") + + # patch assertions + mock_get_locale.assert_any_call() + mock_redirect_error.assert_called_once_with(msg="Not implemented for 'mockos'") + mock_environ_get.assert_called_once_with("USER") + mock_name.assert_has_calls([call(), call(pretty=True)]) + mock_id.assert_has_calls([call(), call()]) + mock_like.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("sys.platform", "linux") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.check_permissions_screen.os.environ.get", + return_value="mockeduser", + ) + @patch("src.app.screens.check_permissions_screen.distro.id", return_value="ubuntu") + @patch("src.app.screens.check_permissions_screen.distro.like") + @patch( + "src.app.screens.check_permissions_screen.distro.name", return_value="Ubuntu" + ) + def test_update_check_user_in_ubuntu( + self, mock_name, mock_like, mock_id, mock_environ_get, mock_get_locale + ): + screen = CheckPermissionsScreen() + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + screen.user = "mockeduser" + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + button = grid.children[0] + fontsize = window.size[0] // 24 + + # Do the test + screen.update(name="CheckPermissionsScreen", key="check_user") + message = "".join( + [ + f"[size={fontsize}sp]", + "[color=#efcc00]Setup mockeduser for Ubuntu[/color]", + "[/size]", + ] + ) + + # default assertions + self.assertEqual(button.text, message) + + # patch assertions + mock_get_locale.assert_any_call() + mock_environ_get.assert_called_once_with("USER") + mock_name.assert_called_once() + mock_id.assert_called_once() + mock_like.assert_not_called() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("sys.platform", "linux") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.check_permissions_screen.os.environ.get", + return_value="mockeduser", + ) + @patch("src.app.screens.check_permissions_screen.distro.id", return_value="fedora") + @patch("src.app.screens.check_permissions_screen.distro.like") + @patch( + "src.app.screens.check_permissions_screen.distro.name", return_value="Fedora" + ) + def test_update_check_user_in_fedora( + self, mock_name, mock_like, mock_id, mock_environ_get, mock_get_locale + ): + screen = CheckPermissionsScreen() + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + screen.user = "mockeduser" + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + button = grid.children[0] + fontsize = window.size[0] // 24 + + # Do the test + screen.update(name="CheckPermissionsScreen", key="check_user") + message = "".join( + [ + f"[size={fontsize}sp]", + "[color=#efcc00]", + "Setup mockeduser for Fedora", + "[/color]", + "[/size]", + ] + ) + + # default assertions + self.assertEqual(button.text, message) + + # patch assertions + mock_get_locale.assert_any_call() + mock_environ_get.assert_called_once_with("USER") + mock_name.assert_called_once() + mock_id.assert_called_once() + mock_like.assert_not_called() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("sys.platform", "linux") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.check_permissions_screen.os.environ.get", + return_value="mockeduser", + ) + @patch( + "src.app.screens.check_permissions_screen.distro.id", return_value="linuxmint" + ) + @patch("src.app.screens.check_permissions_screen.distro.like") + @patch("src.app.screens.check_permissions_screen.distro.name", return_value="Mint") + def test_update_check_user_in_linuxmint( + self, mock_name, mock_like, mock_id, mock_environ_get, mock_get_locale + ): + screen = CheckPermissionsScreen() + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + screen.user = "mockeduser" + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + button = grid.children[0] + fontsize = window.size[0] // 24 + + # Do the test + screen.update(name="CheckPermissionsScreen", key="check_user") + message = "".join( + [ + f"[size={fontsize}sp]", + "[color=#efcc00]", + "Setup mockeduser for Mint", + "[/color]", + "[/size]", + ] + ) + + # default assertions + self.assertEqual(button.text, message) + + # patch assertions + mock_get_locale.assert_any_call() + mock_environ_get.assert_called_once_with("USER") + mock_name.assert_called_once() + mock_id.assert_called_once() + mock_like.assert_not_called() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("sys.platform", "linux") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.check_permissions_screen.os.environ.get", + return_value="mockeduser", + ) + @patch("src.app.screens.check_permissions_screen.distro.id", return_value="mockos") + @patch( + "src.app.screens.check_permissions_screen.distro.like", return_value="debian" + ) + @patch( + "src.app.screens.check_permissions_screen.distro.name", return_value="Mockos" + ) + def test_update_check_user_in_debian_like( + self, mock_name, mock_like, mock_id, mock_environ_get, mock_get_locale + ): + screen = CheckPermissionsScreen() + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + screen.user = "mockeduser" + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + button = grid.children[0] + fontsize = window.size[0] // 24 + + # Do the test + screen.update(name="CheckPermissionsScreen", key="check_user") + message = "".join( + [ + f"[size={fontsize}sp]", + "[color=#efcc00]", + "Setup mockeduser for Mockos", + "[/color]", + "[/size]", + ] + ) + + # default assertions + self.assertEqual(button.text, message) + + # patch assertions + mock_get_locale.assert_any_call() + mock_environ_get.assert_called_once_with("USER") + mock_name.assert_called_once() + mock_id.assert_has_calls([call(), call()]) + mock_like.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("sys.platform", "linux") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.check_permissions_screen.os.environ.get", + return_value="mockeduser", + ) + @patch("src.app.screens.check_permissions_screen.distro.id", return_value="arch") + @patch("src.app.screens.check_permissions_screen.distro.like") + @patch( + "src.app.screens.check_permissions_screen.distro.name", return_value="ArchLinux" + ) + def test_check_user_in_arch( + self, mock_name, mock_like, mock_id, mock_environ_get, mock_get_locale + ): + screen = CheckPermissionsScreen() + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + screen.user = "mockeduser" + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + button = grid.children[0] + fontsize = window.size[0] // 24 + + # Do the test + screen.update(name="CheckPermissionsScreen", key="check_user") + message = "".join( + [ + f"[size={fontsize}sp]", + "[color=#efcc00]Setup mockeduser for ArchLinux[/color]", + "[/size]", + ] + ) + + # default assertions + self.assertEqual(button.text, message) + + # patch assertions + mock_get_locale.assert_any_call() + mock_environ_get.assert_called_once_with("USER") + mock_name.assert_called_once() + mock_id.assert_has_calls([call(), call()]) + mock_like.assert_not_called() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("sys.platform", "linux") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.check_permissions_screen.os.environ.get", + return_value="mockeduser", + ) + @patch("src.app.screens.check_permissions_screen.distro.id", return_value="manjaro") + @patch("src.app.screens.check_permissions_screen.distro.like") + @patch( + "src.app.screens.check_permissions_screen.distro.name", return_value="Manjaro" + ) + def test_update_check_user_in_manjaro( + self, mock_name, mock_like, mock_id, mock_environ_get, mock_get_locale + ): + screen = CheckPermissionsScreen() + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + screen.user = "mockeduser" + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + button = grid.children[0] + fontsize = window.size[0] // 24 + + # Do the test + screen.update(name="CheckPermissionsScreen", key="check_user") + message = "".join( + [ + f"[size={fontsize}sp]", + "[color=#efcc00]Setup mockeduser for Manjaro[/color]", + "[/size]", + ] + ) + + # default assertions + self.assertEqual(button.text, message) + + # patch assertions + mock_get_locale.assert_any_call() + mock_environ_get.assert_called_once_with("USER") + mock_name.assert_called_once() + mock_id.assert_has_calls([call(), call()]) + mock_like.assert_not_called() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("sys.platform", "linux") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.check_permissions_screen.os.environ.get", + return_value="mockeduser", + ) + @patch( + "src.app.screens.check_permissions_screen.distro.id", return_value="slackware" + ) + @patch("src.app.screens.check_permissions_screen.distro.like") + @patch( + "src.app.screens.check_permissions_screen.distro.name", return_value="Slackware" + ) + def test_update_check_user_in_slackware( + self, mock_name, mock_like, mock_id, mock_environ_get, mock_get_locale + ): + screen = CheckPermissionsScreen() + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + screen.user = "mockeduser" + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + button = grid.children[0] + fontsize = window.size[0] // 24 + + # Do the test + screen.update(name="CheckPermissionsScreen", key="check_user") + message = "".join( + [ + f"[size={fontsize}sp]", + "[color=#efcc00]Setup mockeduser for Slackware[/color]", + "[/size]", + ] + ) + + # default assertions + self.assertEqual(button.text, message) + + # patch assertions + mock_get_locale.assert_any_call() + mock_environ_get.assert_called_once_with("USER") + mock_name.assert_called_once() + mock_id.assert_has_calls([call(), call()]) + mock_like.assert_not_called() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("sys.platform", "linux") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.check_permissions_screen.os.environ.get", + return_value="mockeduser", + ) + @patch("src.app.screens.check_permissions_screen.distro.id", return_value="gentoo") + @patch("src.app.screens.check_permissions_screen.distro.like") + @patch( + "src.app.screens.check_permissions_screen.distro.name", return_value="Gentoo" + ) + def test_update_check_user_in_gentoo( + self, mock_name, mock_like, mock_id, mock_environ_get, mock_get_locale + ): + screen = CheckPermissionsScreen() + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + screen.user = "mockeduser" + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + button = grid.children[0] + fontsize = window.size[0] // 24 + + # Do the test + screen.update(name="CheckPermissionsScreen", key="check_user") + message = "".join( + [ + f"[size={fontsize}sp]", + "[color=#efcc00]", + "Setup mockeduser for Gentoo", + "[/color]", + "[/size]", + ] + ) + + # default assertions + self.assertEqual(button.text, message) + + # patch assertions + mock_get_locale.assert_any_call() + mock_environ_get.assert_called_once_with("USER") + mock_name.assert_called_once() + mock_id.assert_has_calls([call(), call()]) + mock_like.assert_not_called() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("sys.platform", "linux") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.check_permissions_screen.distro.name", return_value="MockOS" + ) + @patch("src.app.screens.check_permissions_screen.grp") + def test_check_group_not_in_group( + self, mock_grp, mock_distro_name, mock_get_locale + ): + mocked_group = MagicMock() + mocked_group.gr_name = "mockedgroup" + mocked_group.__iter__ = MagicMock(return_value=iter([0, 1, 2, 3])) + + attrs = { + "getgrall.return_value": [mocked_group], + } + mock_grp.configure_mock(**attrs) + + screen = CheckPermissionsScreen() + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + screen.user = "mockeduser" + screen.bin = "mock" + screen.bin_args = ["-a", "-G"] + screen.group = "mockedgroup" + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # Do the test + screen.update(name="CheckPermissionsScreen", key="check_group") + + # default assertions + # self.assertEqual(button.text, message) + self.assertEqual(screen.in_dialout, False) + + # patch assertions + mock_get_locale.assert_any_call() + mock_distro_name.assert_called_once_with(pretty=True) + mock_grp.getgrall.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("sys.platform", "linux") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_make_on_permission_created(self, mock_get_locale): + screen = CheckPermissionsScreen() + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + screen.user = "mockeduser" + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + button = grid.children[0] + fontsize_m = window.size[0] // 32 + + # Do the test + screen.update(name="CheckPermissionsScreen", key="make_on_permission_created") + text = "\n".join( + [ + f"[size={fontsize_m}sp][color=#efcc00]mocked command[/color][/size]", + "", + f"[size={fontsize_m}sp]You may need to logout (or even reboot)", + "and back in for the new group to take effect.", + "", + "Do not worry, this message won't appear again.[/size]", + ] + ) + + # pylint: disable=not-callable + on_permission_created = getattr(screen, "on_permission_created") + on_permission_created(output="mocked command") + + # default assertions + self.assertEqual(button.text, text) + self.assertEqual(screen.bin, None) + self.assertEqual(screen.bin_args, None) + self.assertEqual(screen.group, None) + self.assertEqual(screen.user, None) + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("sys.platform", "mockos") + @patch("src.app.screens.base_screen.BaseScreen.get_locale") + @patch("src.app.screens.check_permissions_screen.partial") + def test_on_enter(self, mock_partial, mock_get_locale): + screen = CheckPermissionsScreen() + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + screen.user = "mockeduser" + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # Do the test + screen.on_enter() + + # patch assertions + mock_get_locale.assert_called_once() + mock_partial.assert_has_calls( + [ + call(screen.update, name="CheckPermissionsScreen", key="canvas"), + call(screen.update, name="CheckPermissionsScreen", key="check_user"), + ], + any_order=True, + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.BaseScreen.get_locale") + def test_render_button(self, mock_get_locale): + screen = CheckPermissionsScreen() + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + self.assertTrue("check_permissions_screen_label" in screen.ids) + + # patch assertions + mock_get_locale.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("sys.platform", "linux") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + @patch("src.app.screens.check_permissions_screen.SudoerLinux.exec") + def test_fail_on_ref_press_allow( + self, mock_exec, mock_redirect_error, mock_get_locale + ): + screen = CheckPermissionsScreen() + screen.manager = MagicMock() + screen.manager.get_screen = MagicMock() + screen.bin = "mock" + screen.bin_args = ["-a", "-G"] + screen.group = "mockedgroup" + screen.user = "mockeduser" + screen.on_permission_created = None + + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + action = getattr(screen, f"on_ref_press_{screen.id}_label") + action("Allow") + + # patch assertions + mock_get_locale.assert_called_once() + mock_redirect_error.assert_called_once_with( + msg="Invalid on_permission_created: None" + ) + mock_exec.assert_not_called() diff --git a/e2e/test_003_main_screen.py b/e2e/test_003_main_screen.py new file mode 100644 index 00000000..f8b395f8 --- /dev/null +++ b/e2e/test_003_main_screen.py @@ -0,0 +1,699 @@ +import os +import re +from unittest.mock import patch, call, MagicMock +from kivy.base import EventLoop, EventLoopBase +from kivy.tests.common import GraphicUnitTest +from kivy.core.text import LabelBase, DEFAULT_FONT +from src.app.screens.main_screen import MainScreen + + +class TestMainScreen(GraphicUnitTest): + + @classmethod + def setUpClass(cls): + cwd_path = os.path.dirname(__file__) + rel_assets_path = os.path.join(cwd_path, "..", "assets") + assets_path = os.path.abspath(rel_assets_path) + noto_sans_path = os.path.join(assets_path, "NotoSansCJK_Cy_SC_KR_Krux.ttf") + LabelBase.register(DEFAULT_FONT, noto_sans_path) + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_render_main_screen(self, mock_get_locale): + screen = MainScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + self.assertEqual(window.children[0], screen) + self.assertEqual(screen.name, "MainScreen") + self.assertEqual(screen.id, "main_screen") + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_render_grid_layout(self, mock_get_locale): + mock_get_locale.config = MagicMock() + mock_get_locale.config.get = MagicMock(return_value="en-US") + screen = MainScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + self.assertEqual(grid.id, "main_screen_grid") + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_render_buttons(self, mock_get_locale): + screen = MainScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + buttons = grid.children + + self.assertEqual(len(buttons), 6) + self.assertEqual(buttons[5].id, "main_select_version") + self.assertEqual(buttons[4].id, "main_select_device") + self.assertEqual(buttons[3].id, "main_flash") + self.assertEqual(buttons[2].id, "main_wipe") + self.assertEqual(buttons[1].id, "main_settings") + self.assertEqual(buttons[0].id, "main_about") + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.MainScreen.set_background") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_on_press_cant_flash_or_wipe(self, mock_get_locale, mock_set_background): + screen = MainScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + calls = [] + for button in grid.children: + action = getattr(screen, f"on_press_{button.id}") + action(button) + if button.id in ( + "main_select_device", + "main_select_version", + "main_settings", + "main_about", + ): + calls.append(call(wid=button.id, rgba=(0.25, 0.25, 0.25, 1))) + + mock_set_background.assert_has_calls(calls) + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.MainScreen.set_background") + @patch("src.app.screens.main_screen.MainScreen.set_screen") + @patch("src.app.screens.main_screen.MainScreen.manager") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.open_settings") + def test_on_release_cant_flash_or_wipe( + self, + mock_open_settings, + mock_get_locale, + mock_manager, + mock_set_screen, + mock_set_background, + ): + mock_manager.get_screen = MagicMock() + + screen = MainScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + calls_set_background = [] + calls_set_screen = [] + calls_manager = [] + + for button in grid.children: + action = getattr(screen, f"on_release_{button.id}") + action(button) + + if button.id in ( + "main_select_device", + "main_select_version", + "main_settings", + "main_about", + ): + calls_set_background.append(call(wid=button.id, rgba=(0, 0, 0, 1))) + + if button.id == "main_select_device": + calls_set_screen.append( + call(name="SelectDeviceScreen", direction="left") + ) + calls_manager.append(call("SelectDeviceScreen")) + + if button.id == "main_select_version": + calls_set_screen.append( + call(name="SelectVersionScreen", direction="left") + ) + calls_manager.append(call("SelectVersionScreen")) + calls_manager.append(call().clear()) + calls_manager.append(call().fetch_releases()) + if button.id == "main_about": + calls_set_screen.append(call(name="AboutScreen", direction="left")) + + mock_set_background.assert_has_calls(calls_set_background) + mock_set_screen.assert_has_calls(calls_set_screen) + mock_manager.get_screen.assert_has_calls(calls_manager) + mock_get_locale.assert_any_call() + mock_open_settings.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_version(self, mock_get_locale): + screen = MainScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + device_button = grid.children[5] + flash_button = grid.children[3] + wipe_button = grid.children[2] + + text_device = "Version: [color=#00AABB]select a new one[/color]" + text_flash = "[color=#333333]Flash firmware[/color]" + text_wipe = "[color=#333333]Wipe device[/color]" + calls = [] + + self.assertEqual(device_button.text, text_device) + self.assertEqual(flash_button.text, text_flash) + self.assertEqual(wipe_button.text, text_wipe) + + for version in ( + "odudex/krux_binaries", + "v23.09.1", + "v23.09.0", + "v22.08.2", + "v22.08.1", + "v22.08.0", + "v22.03.0", + ): + text_version = f"Version: [color=#00AABB]{version}[/color]" + screen.update(name="SelectVersionScreen", key="version", value=version) + self.assertEqual(device_button.text, text_version) + self.assertEqual(flash_button.text, text_flash) + self.assertEqual(wipe_button.text, text_wipe) + + # each button has at least 2 calls of get locale + # one for locale, other for font + call.append(call()) + call.append(call()) + + mock_get_locale.assert_has_calls(calls) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + # pylint: disable=too-many-locals + def test_update_device(self, mock_get_locale): + screen = MainScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + device_button = grid.children[4] + flash_button = grid.children[3] + wipe_button = grid.children[2] + + text_device = "Device: [color=#00AABB]select a new one[/color]" + mocked_text_device = "Device: [color=#00AABB]Mocked device[/color]" + text_flash = "[color=#333333]Flash firmware[/color]" + mocked_text_flash = "Flash firmware" + text_wipe = "[color=#333333]Wipe device[/color]" + mocked_text_wipe = "Wipe device" + + self.assertEqual(device_button.text, text_device) + self.assertEqual(flash_button.text, text_flash) + self.assertEqual(wipe_button.text, text_wipe) + + for device in ("m5stickv", "amigo", "dock", "bit", "yahboom", "cube"): + screen.update(name="SelectVersionScreen", key="device", value=device) + mocked_text_device = f"Device: [color=#00AABB]{device}[/color]" + self.assertEqual(device_button.text, mocked_text_device) + self.assertEqual(flash_button.text, mocked_text_flash) + self.assertEqual(wipe_button.text, mocked_text_wipe) + + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update_invalid_screen(self, mock_redirect_error, mock_get_locale): + screen = MainScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + screen.update(name="MockedScreen", key="device", value="v24.03.0") + + mock_get_locale.assert_any_call() + mock_redirect_error.assert_called_once_with( + msg="Invalid screen name: MockedScreen" + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update_invalid_key(self, mock_redirect_error, mock_get_locale): + screen = MainScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + screen.update(name="SelectDeviceScreen", key="mock", value="mock") + mock_get_locale.assert_any_call() + mock_redirect_error.assert_called_once_with(msg='Invalid key: "mock"') + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_no_valid_device_but_valid_situation(self, mock_get_locale): + screen = MainScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + device_button = grid.children[4] + flash_button = grid.children[3] + wipe_button = grid.children[2] + + text_device = "Device: [color=#00AABB]select a new one[/color]" + mocked_text_device = "Device: [color=#00AABB]Mocked device[/color]" + text_flash = "[color=#333333]Flash firmware[/color]" + text_wipe = "[color=#333333]Wipe device[/color]" + + self.assertEqual(device_button.text, text_device) + self.assertEqual(flash_button.text, text_flash) + self.assertEqual(wipe_button.text, text_wipe) + + screen.update(name="SelectDeviceScreen", key="device", value="Mocked device") + + self.assertEqual(device_button.text, mocked_text_device) + self.assertEqual(flash_button.text, text_flash) + self.assertEqual(wipe_button.text, text_wipe) + + # each button has at least 2 calls of get locale + # one for locale, other for font + # and since update device, two more calls + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_locale(self, mock_get_locale): + screen = MainScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + screen.update(name="ConfigKruxInstaller", key="locale", value="en_US.UTF-8") + + self.assertEqual(screen.locale, "en_US.UTF-8") + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_device_select_a_new_one(self, mock_get_locale): + screen = MainScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + device_button = grid.children[4] + + screen.update( + name="ConfigKruxInstaller", key="device", value="select a new one" + ) + text_device = "Device: [color=#00AABB]select a new one[/color]" + self.assertEqual(screen.device, "select a new one") + self.assertEqual(device_button.text, text_device) + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_flash_not_will_flash(self, mock_get_locale): + screen = MainScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + flash_button = grid.children[3] + + screen.update(name="ConfigKruxInstaller", key="flash", value=None) + + text_flash = "[color=#333333]Flash firmware[/color]" + self.assertTrue(flash_button.markup) + self.assertEqual(flash_button.text, text_flash) + + # each button has at least 2 calls of get locale + # one for locale, other for font + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_flash_will_flash(self, mock_get_locale): + screen = MainScreen() + screen.will_flash = True + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + flash_button = grid.children[3] + + screen.update(name="ConfigKruxInstaller", key="flash", value=None) + + text_flash = "Flash firmware" + self.assertEqual(flash_button.text, text_flash) + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_flash_not_will_wipe(self, mock_get_locale): + mock_get_locale.config = MagicMock() + mock_get_locale.config.get = MagicMock(return_value="en-US") + + screen = MainScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + wipe_button = grid.children[2] + + screen.update(name="ConfigKruxInstaller", key="wipe", value=None) + + text_wipe = "[color=#333333]Wipe device[/color]" + self.assertEqual(wipe_button.text, text_wipe) + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_flash_will_wipe(self, mock_get_locale): + screen = MainScreen() + screen.will_wipe = True + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + wipe_button = grid.children[2] + + screen.update(name="ConfigKruxInstaller", key="wipe", value=None) + text_wipe = "Wipe device" + + self.assertEqual(wipe_button.text, text_wipe) + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_settings(self, mock_get_locale): + screen = MainScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + s_button = grid.children[1] + + screen.update(name="ConfigKruxInstaller", key="settings", value=None) + + settings_text = "Settings" + self.assertEqual(s_button.text, settings_text) + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_about(self, mock_get_locale): + screen = MainScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + a_button = grid.children[0] + + screen.update(name="ConfigKruxInstaller", key="about", value=None) + about_text = "About" + self.assertEqual(a_button.text, about_text) + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.MainScreen.set_background") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_on_press_can_flash_or_wipe(self, mock_get_locale, mock_set_background): + screen = MainScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + flash_button = grid.children[3] + wipe_button = grid.children[2] + + background_calls = [] + + for device in ("m5stickv", "amigo", "dock", "bit", "yahboom", "cube"): + screen.update(name="SelectVersionScreen", key="device", value=device) + flash_action = getattr(screen, "on_press_main_flash") + wipe_action = getattr(screen, "on_press_main_wipe") + flash_action(flash_button) + wipe_action(wipe_button) + background_calls.append(call(wid="main_flash", rgba=(0.25, 0.25, 0.25, 1))) + background_calls.append(call(wid="main_wipe", rgba=(0.25, 0.25, 0.25, 1))) + + mock_set_background.assert_has_calls(background_calls) + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.MainScreen.set_background") + @patch("src.app.screens.main_screen.MainScreen.set_screen") + @patch("src.app.screens.main_screen.MainScreen.manager") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.get_destdir_assets") + @patch("src.app.screens.main_screen.re.findall", side_effect=[True]) + @patch("src.app.screens.main_screen.os.path.isfile", side_effect=[False]) + def test_on_release_flash_to_download_stable_zip_screen( + self, + mock_isfile, + mock_findall, + mock_get_destdir_assets, + mock_get_locale, + mock_manager, + mock_set_screen, + mock_set_background, + ): + mock_manager.get_screen = MagicMock() + + screen = MainScreen() + screen.version = "v24.03.0" + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + flash_button = grid.children[3] + + screen.update(name="SelectVersionScreen", key="device", value="m5stickv") + flash_action = getattr(screen, "on_release_main_flash") + flash_action(flash_button) + + mock_get_locale.assert_any_call() + mock_get_destdir_assets.assert_called_once() + mock_set_background.assert_called_once_with(wid="main_flash", rgba=(0, 0, 0, 1)) + mock_set_screen.assert_called_once_with( + name="DownloadStableZipScreen", direction="left" + ) + mock_findall.assert_called_once_with(r"^v\d+\.\d+\.\d$", "v24.03.0") + pattern = re.compile(r".*v24\.03\.0\.zip") + self.assertTrue(pattern.match(mock_isfile.call_args[0][0])) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.MainScreen.set_background") + @patch("src.app.screens.main_screen.MainScreen.set_screen") + @patch("src.app.screens.main_screen.MainScreen.manager") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.get_destdir_assets") + @patch("src.app.screens.main_screen.re.findall", side_effect=[True]) + @patch("src.app.screens.main_screen.os.path.isfile", side_effect=[True]) + def test_on_release_flash_to_warning_already_downloaded_zip_screen( + self, + mock_isfile, + mock_findall, + mock_get_destdir_assets, + mock_get_locale, + mock_manager, + mock_set_screen, + mock_set_background, + ): + mock_manager.get_screen = MagicMock() + + screen = MainScreen() + screen.version = "v24.03.0" + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + flash_button = grid.children[3] + + screen.update(name="SelectVersionScreen", key="device", value="m5stickv") + flash_action = getattr(screen, "on_release_main_flash") + flash_action(flash_button) + + mock_get_locale.assert_any_call() + mock_get_destdir_assets.assert_called_once() + mock_set_background.assert_called_once_with(wid="main_flash", rgba=(0, 0, 0, 1)) + mock_set_screen.assert_called_once_with( + name="WarningAlreadyDownloadedScreen", direction="left" + ) + mock_findall.assert_called_once_with(r"^v\d+\.\d+\.\d$", "v24.03.0") + pattern = re.compile(r".*v24\.03\.0\.zip") + self.assertTrue(pattern.match(mock_isfile.call_args[0][0])) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.MainScreen.set_background") + @patch("src.app.screens.main_screen.MainScreen.set_screen") + @patch("src.app.screens.main_screen.MainScreen.manager") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.main_screen.re.findall", side_effect=[False, True]) + def test_on_release_flash_to_download_beta_screen( + self, + mock_findall, + mock_get_locale, + mock_manager, + mock_set_screen, + mock_set_background, + ): + mock_manager.get_screen = MagicMock() + + screen = MainScreen() + screen.version = "odudex/krux_binaries" + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + flash_button = grid.children[3] + + screen.update(name="SelectVersionScreen", key="device", value="m5stickv") + flash_action = getattr(screen, "on_release_main_flash") + flash_action(flash_button) + + mock_get_locale.assert_any_call() + mock_findall.assert_has_calls( + [ + call("^v\\d+\\.\\d+\\.\\d$", "odudex/krux_binaries"), + call(r"^odudex/krux_binaries", "odudex/krux_binaries"), + ] + ) + mock_set_background.assert_called_once_with(wid="main_flash", rgba=(0, 0, 0, 1)) + mock_set_screen.assert_called_once_with( + name="DownloadBetaScreen", direction="left" + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.MainScreen.set_background") + @patch("src.app.screens.main_screen.MainScreen.set_screen") + @patch("src.app.screens.main_screen.MainScreen.manager") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_on_release_wipe( + self, + mock_get_locale, + mock_manager, + mock_set_screen, + mock_set_background, + ): + mock_manager.get_screen = MagicMock() + + screen = MainScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + wipe_button = grid.children[2] + + calls_set_background = [] + calls_set_screen = [] + + for device in ("m5stickv", "amigo", "dock", "bit", "yahboom", "cube"): + screen.update(name="SelectVersionScreen", key="device", value=device) + wipe_action = getattr(screen, "on_release_main_wipe") + wipe_action(wipe_button) + + calls_set_background.append(call(wid="main_wipe", rgba=(0, 0, 0, 1))) + calls_set_screen.append(call(name="WarningWipeScreen", direction="left")) + + mock_get_locale.assert_any_call() + mock_set_background.assert_has_calls(calls_set_background) + mock_set_screen.assert_has_calls(calls_set_screen) diff --git a/e2e/test_004_select_device_screen.py b/e2e/test_004_select_device_screen.py new file mode 100644 index 00000000..c6aa608b --- /dev/null +++ b/e2e/test_004_select_device_screen.py @@ -0,0 +1,327 @@ +from unittest.mock import patch, call, MagicMock +from kivy.base import EventLoop, EventLoopBase +from kivy.tests.common import GraphicUnitTest +from src.app.screens.select_device_screen import SelectDeviceScreen + + +class TestSelectDeviceScreen(GraphicUnitTest): + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.App.get_running_app") + def test_render_main_screen(self, mock_get_running_app): + mock_get_running_app.config = MagicMock() + mock_get_running_app.config.get = MagicMock(return_value="en-US") + + screen = SelectDeviceScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + self.assertEqual(window.children[0], screen) + self.assertEqual(screen.name, "SelectDeviceScreen") + self.assertEqual(screen.id, "select_device_screen") + + mock_get_running_app.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.App.get_running_app") + def test_render_grid_layout(self, mock_get_running_app): + mock_get_running_app.config = MagicMock() + mock_get_running_app.config.get = MagicMock(return_value="en-US") + + screen = SelectDeviceScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + self.assertEqual(grid.id, "select_device_screen_grid") + mock_get_running_app.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.App.get_running_app") + def test_render_buttons(self, mock_get_running_app): + mock_get_running_app.config = MagicMock() + mock_get_running_app.config.get = MagicMock(return_value="en-US") + + screen = SelectDeviceScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + buttons = grid.children + + self.assertEqual(len(buttons), 7) + self.assertEqual(buttons[6].id, "select_device_m5stickv") + self.assertEqual(buttons[5].id, "select_device_amigo") + self.assertEqual(buttons[4].id, "select_device_dock") + self.assertEqual(buttons[3].id, "select_device_bit") + self.assertEqual(buttons[2].id, "select_device_yahboom") + self.assertEqual(buttons[1].id, "select_device_cube") + self.assertEqual(buttons[0].id, "select_device_wonder_mv") + mock_get_running_app.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.App.get_running_app") + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update(self, mock_redirect_error, mock_get_running_app): + mock_get_running_app.config = MagicMock() + mock_get_running_app.config.get = MagicMock(return_value="en-US") + + screen = SelectDeviceScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + screen.update(name=screen.name, key="mock", value="mock") + + mock_get_running_app.assert_called_once() + mock_redirect_error.assert_called_once_with("Invalid key: mock") + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.App.get_running_app") + @patch("src.app.screens.select_device_screen.SelectDeviceScreen.set_background") + def test_on_press_with_latest_version( + self, mock_set_background, mock_get_running_app + ): + mock_get_running_app.config = MagicMock() + mock_get_running_app.config.get = MagicMock(return_value="en-US") + + screen = SelectDeviceScreen() + self.render(screen) + screen.update(name=screen.name, key="version", value="v24.07.0") + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + calls = [] + + for button in grid.children: + action = getattr(screen, f"on_press_{button.id}") + action(button) + if button.id in ( + "select_device_m5stickv", + "select_device_amigo", + "select_device_dock", + "select_device_bit", + "select_device_yahboom", + ): + calls.append(call(wid=button.id, rgba=(0.25, 0.25, 0.25, 1))) + + mock_set_background.assert_has_calls(calls) + mock_get_running_app.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.App.get_running_app") + @patch("src.app.screens.select_device_screen.SelectDeviceScreen.set_background") + def test_on_press_with_older_version( + self, mock_set_background, mock_get_running_app + ): + mock_get_running_app.config = MagicMock() + mock_get_running_app.config.get = MagicMock(return_value="en-US") + + screen = SelectDeviceScreen() + self.render(screen) + screen.update(name=screen.name, key="version", value="v24.03.0") + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + calls = [] + + for button in grid.children: + action = getattr(screen, f"on_press_{button.id}") + action(button) + if button.id in ( + "select_device_m5stickv", + "select_device_amigo", + "select_device_dock", + "select_device_bit", + ): + calls.append(call(wid=button.id, rgba=(0.25, 0.25, 0.25, 1))) + + mock_set_background.assert_has_calls(calls) + mock_get_running_app.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.App.get_running_app") + @patch("src.app.screens.select_device_screen.SelectDeviceScreen.set_background") + def test_on_press_with_beta_version( + self, mock_set_background, mock_get_running_app + ): + mock_get_running_app.config = MagicMock() + mock_get_running_app.config.get = MagicMock(return_value="en-US") + + screen = SelectDeviceScreen() + self.render(screen) + screen.update(name=screen.name, key="version", value="odudex/krux_binaries") + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + calls = [] + + for button in grid.children: + action = getattr(screen, f"on_press_{button.id}") + action(button) + if button.id in ( + "select_device_m5stickv", + "select_device_amigo", + "select_device_dock", + "select_device_bit", + "select_device_yahboom", + "select_device_cube", + ): + calls.append(call(wid=button.id, rgba=(0.25, 0.25, 0.25, 1))) + + mock_set_background.assert_has_calls(calls) + mock_get_running_app.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.App.get_running_app") + @patch("src.app.screens.select_device_screen.SelectDeviceScreen.set_background") + @patch("src.app.screens.select_device_screen.SelectDeviceScreen.manager") + @patch("src.app.screens.select_device_screen.SelectDeviceScreen.set_screen") + def test_on_release_with_latest_version( + self, mock_set_screen, mock_manager, mock_set_background, mock_get_running_app + ): + mock_manager.get_screen = MagicMock() + + mock_get_running_app.config = MagicMock() + mock_get_running_app.config.get = MagicMock(return_value="en-US") + + screen = SelectDeviceScreen() + screen.update(key="version", value="v24.03.0") + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + calls_set_background = [] + calls_manager = [] + calls_set_screen = [] + + for button in grid.children: + action = getattr(screen, f"on_release_{button.id}") + action(button) + + if button.id in ( + "select_device_m5stickv", + "select_device_amigo", + "select_device_dock", + "select_device_bit", + "select_device_yahboom", + ): + calls_set_background.append(call(wid=button.id, rgba=(0, 0, 0, 1))) + calls_manager.append(call("MainScreen")) + calls_set_screen.append(call(name="MainScreen", direction="right")) + + mock_set_background.assert_has_calls(calls_set_background) + mock_manager.get_screen.assert_has_calls(calls_manager) + mock_set_screen.assert_has_calls(calls_set_screen) + mock_get_running_app.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.App.get_running_app") + @patch("src.app.screens.select_device_screen.SelectDeviceScreen.set_background") + @patch("src.app.screens.select_device_screen.SelectDeviceScreen.manager") + @patch("src.app.screens.select_device_screen.SelectDeviceScreen.set_screen") + def test_on_release_with_beta_version( + self, mock_set_screen, mock_manager, mock_set_background, mock_get_running_app + ): + mock_manager.get_screen = MagicMock() + + mock_get_running_app.config = MagicMock() + mock_get_running_app.config.get = MagicMock(return_value="en-US") + + screen = SelectDeviceScreen() + screen.update(key="version", value="odudex/krux_binaries") + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + calls_set_background = [] + calls_manager = [] + calls_set_screen = [] + + for button in grid.children: + action = getattr(screen, f"on_release_{button.id}") + action(button) + + if button.id in ( + "select_device_m5stickv", + "select_device_amigo", + "select_device_dock", + "select_device_bit", + "select_device_yahboom", + "select_device_cube", + ): + calls_set_background.append(call(wid=button.id, rgba=(0, 0, 0, 1))) + calls_manager.append(call("MainScreen")) + calls_set_screen.append(call(name="MainScreen", direction="right")) + + mock_set_background.assert_has_calls(calls_set_background) + mock_manager.get_screen.assert_has_calls(calls_manager) + mock_set_screen.assert_has_calls(calls_set_screen) + mock_get_running_app.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.main_screen.App.get_running_app") + @patch("src.app.screens.select_device_screen.SelectDeviceScreen.set_background") + @patch("src.app.screens.select_device_screen.SelectDeviceScreen.manager") + @patch("src.app.screens.select_device_screen.SelectDeviceScreen.set_screen") + def test_on_release_with_v22_03_0_version( + self, mock_set_screen, mock_manager, mock_set_background, mock_get_running_app + ): + mock_manager.get_screen = MagicMock() + + mock_get_running_app.config = MagicMock() + mock_get_running_app.config.get = MagicMock(return_value="en-US") + + screen = SelectDeviceScreen() + screen.update(key="version", value="v22.03.0") + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + calls_set_background = [] + calls_manager = [] + calls_set_screen = [] + + for button in grid.children: + action = getattr(screen, f"on_release_{button.id}") + action(button) + + if button.id in ("select_device_m5stickv"): + calls_set_background.append(call(wid=button.id, rgba=(0, 0, 0, 1))) + calls_manager.append(call("MainScreen")) + calls_set_screen.append(call(name="MainScreen", direction="right")) + + mock_set_background.assert_has_calls(calls_set_background) + mock_manager.get_screen.assert_has_calls(calls_manager) + mock_set_screen.assert_has_calls(calls_set_screen) + mock_get_running_app.assert_called_once() diff --git a/e2e/test_005_select_version_screen.py b/e2e/test_005_select_version_screen.py new file mode 100644 index 00000000..1345a51f --- /dev/null +++ b/e2e/test_005_select_version_screen.py @@ -0,0 +1,278 @@ +from unittest.mock import patch, call, MagicMock +from kivy.base import EventLoop, EventLoopBase +from kivy.tests.common import GraphicUnitTest +from src.app.screens.select_version_screen import SelectVersionScreen + +MOCKED_FOUND_API = [ + {"author": "test", "tag_name": "v24.03.0"}, + {"author": "test", "tag_name": "v23.08.1"}, + {"author": "test", "tag_name": "v23.08.0"}, + {"author": "test", "tag_name": "v22.08.1"}, + {"author": "test", "tag_name": "v22.08.0"}, + {"author": "test", "tag_name": "v22.02.0"}, +] + + +class TestSelectVersionScreen(GraphicUnitTest): + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_render_select_version_screen(self, mock_get_locale): + screen = SelectVersionScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + self.assertEqual(window.children[0], screen) + self.assertEqual(screen.name, "SelectVersionScreen") + self.assertEqual(screen.id, "select_version_screen") + mock_get_locale.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_render_grid_layout(self, mock_get_locale): + screen = SelectVersionScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + self.assertEqual(grid.id, "select_version_screen_grid") + mock_get_locale.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("kivy.uix.gridlayout.GridLayout.clear_widgets") + def test_clear_grid(self, mock_clear_widgets, mock_get_locale): + screen = SelectVersionScreen() + screen.clear() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + mock_clear_widgets.assert_called_once() + mock_get_locale.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.utils.selector.requests") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.select_version_screen.SelectVersionScreen.manager") + def test_render_buttons(self, mock_manager, mock_get_locale, mock_requests): + # Configure mocks + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = MOCKED_FOUND_API + mock_requests.get.return_value = mock_response + mock_manager.get_screen = MagicMock() + + screen = SelectVersionScreen() + screen.fetch_releases() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + buttons = grid.children + + self.assertEqual(len(buttons), 4) + self.assertEqual(buttons[3].id, "select_version_latest") + self.assertEqual(buttons[2].id, "select_version_beta") + self.assertEqual(buttons[1].id, "select_version_old") + self.assertEqual(buttons[0].id, "select_version_back") + + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.utils.selector.requests") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.select_version_screen.SelectVersionScreen.manager") + @patch("src.app.screens.select_version_screen.SelectVersionScreen.set_background") + def test_on_press( + self, mock_set_background, mock_manager, mock_get_locale, mock_requests + ): + # Configure mocks + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = MOCKED_FOUND_API + mock_requests.get.return_value = mock_response + mock_manager.get_screen = MagicMock() + + screen = SelectVersionScreen() + screen.fetch_releases() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + calls = [] + for button in grid.children: + action = getattr(screen, f"on_press_{button.id}") + action(button) + if button.id in ( + "main_select_device", + "main_select_version", + "main_settings", + "main_about", + ): + calls.append(call(wid=button.id, rgba=(0.25, 0.25, 0.25, 1))) + + mock_set_background.assert_has_calls(calls) + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.utils.selector.requests") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.select_version_screen.SelectVersionScreen.set_background") + @patch("src.app.screens.select_version_screen.SelectVersionScreen.set_screen") + @patch("src.app.screens.select_version_screen.SelectVersionScreen.manager") + def test_on_release( + self, + mock_manager, + mock_set_screen, + mock_set_background, + mock_get_locale, + mock_requests, + ): + # Configure mocks + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = MOCKED_FOUND_API + mock_requests.get.return_value = mock_response + mock_manager.get_screen = MagicMock() + + screen = SelectVersionScreen() + screen.fetch_releases() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + calls_set_background = [] + calls_set_screen = [] + calls_get_screen = [] + + for button in grid.children: + action = getattr(screen, f"on_release_{button.id}") + action(button) + calls_set_background.append(call(wid=button.id, rgba=(0, 0, 0, 1))) + + if button.id == "select_version_stable": + calls_get_screen.append(call("MainScreen")) + calls_set_screen.append(call(name="MainScreen", direction="right")) + + elif button.id == "select_version_beta": + calls_get_screen.append(call("MainScreen")) + calls_set_screen.append( + call(name="WarningBetaScreen", direction="left") + ) + + elif button.id == "select_version_old": + calls_set_screen.append( + call(name="SelectOldVersionScreen", direction="left") + ) + + elif button.id == "select_version_back": + calls_set_screen.append(call(name="MainScreen", direction="right")) + + mock_set_background.assert_has_calls(calls_set_background) + mock_manager.get_screen.assert_has_calls(calls_get_screen) + mock_set_screen.assert_has_calls(calls_set_screen) + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.utils.selector.requests") + @patch("src.app.screens.select_version_screen.SelectVersionScreen.manager") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_locale_old_versions( + self, mock_get_locale, mock_manager, mock_requests + ): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = MOCKED_FOUND_API + mock_requests.get.return_value = mock_response + + mock_manager.get_screen = MagicMock() + + screen = SelectVersionScreen() + screen.fetch_releases() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + button_old_versions = grid.children[1] + button_back = grid.children[0] + + text_old_versions = "Versões antigas" + text_back_button = "Voltar" + screen.update(name="ConfigKruxInstaller", key="locale", value="pt_BR.UTF-8") + + self.assertEqual(button_old_versions.text, text_old_versions) + self.assertEqual(button_back.text, text_back_button) + + mock_manager.get_screen.assert_called_once() + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update_locale_name(self, mock_redirect_error, mock_get_locale): + screen = SelectVersionScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + screen.update(name="Mock", key="locale", value="pt_BR.UTF-8") + + mock_get_locale.assert_called_once() + mock_redirect_error.assert_called_once_with("Invalid screen name: Mock") + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update_locale_key(self, mock_redirect_error, mock_get_locale): + screen = SelectVersionScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + screen.update(name="ConfigKruxInstaller", key="mock", value="pt_BR.UTF-8") + + mock_get_locale.assert_called_once() + mock_redirect_error.assert_called_once_with('Invalid key: "mock"') diff --git a/e2e/test_006_select_old_version_screen.py b/e2e/test_006_select_old_version_screen.py new file mode 100644 index 00000000..bbdb4e85 --- /dev/null +++ b/e2e/test_006_select_old_version_screen.py @@ -0,0 +1,206 @@ +from unittest.mock import patch, call, MagicMock +from kivy.base import EventLoop, EventLoopBase +from kivy.tests.common import GraphicUnitTest +from src.app.screens.select_old_version_screen import SelectOldVersionScreen + +MOCKED_FOUND_API = [ + {"author": "test", "tag_name": "v23.08.1"}, + {"author": "test", "tag_name": "v23.08.0"}, + {"author": "test", "tag_name": "v22.08.1"}, + {"author": "test", "tag_name": "v22.08.0"}, + {"author": "test", "tag_name": "v22.02.0"}, +] + +OLD_VERSIONS = [d["tag_name"] for d in MOCKED_FOUND_API] + + +class TestSelectOldVersionScreen(GraphicUnitTest): + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_render_main_screen(self, mock_get_locale): + screen = SelectOldVersionScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + self.assertEqual(window.children[0], screen) + self.assertEqual(screen.name, "SelectOldVersionScreen") + self.assertEqual(screen.id, "select_old_version_screen") + mock_get_locale.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_render_grid_layout(self, mock_get_locale): + mock_get_locale.config = MagicMock() + mock_get_locale.config.get = MagicMock(return_value="en-US") + + screen = SelectOldVersionScreen() + screen.make_grid( + wid="select_old_version_screen_grid", rows=len(OLD_VERSIONS) + 1 + ) + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + self.assertEqual(grid.id, "select_old_version_screen_grid") + mock_get_locale.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("kivy.uix.gridlayout.GridLayout.clear_widgets") + def test_clear_grid(self, mock_clear_widgets, mock_get_locale): + screen = SelectOldVersionScreen() + screen.make_grid( + wid="select_old_version_screen_grid", rows=len(OLD_VERSIONS) + 1 + ) + screen.clear_grid(wid="select_old_version_screen_grid") + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + mock_clear_widgets.assert_called_once() + mock_get_locale.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.select_old_version_screen.SelectOldVersionScreen.set_background" + ) + def test_on_press(self, mock_set_background, mock_get_locale): + screen = SelectOldVersionScreen() + screen.make_grid( + wid="select_old_version_screen_grid", rows=len(OLD_VERSIONS) + 1 + ) + screen.clear_grid(wid="select_old_version_screen_grid") + screen.fetch_releases(OLD_VERSIONS) + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + self.assertEqual(len(grid.children), len(OLD_VERSIONS) + 1) + + calls = [] + for button in grid.children: + on_press = getattr(screen, f"on_press_{button.id}") + on_press(button) + calls.append(call(wid=button.id, rgba=(0.25, 0.25, 0.25, 1))) + + mock_set_background.assert_has_calls(calls) + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.select_old_version_screen.SelectOldVersionScreen.manager") + @patch( + "src.app.screens.select_old_version_screen.SelectOldVersionScreen.set_background" + ) + @patch( + "src.app.screens.select_old_version_screen.SelectOldVersionScreen.set_screen" + ) + def test_on_release( + self, mock_set_screen, mock_set_background, mock_manager, mock_get_locale + ): + mock_manager.get_screen = MagicMock() + + screen = SelectOldVersionScreen() + screen.make_grid( + wid="select_old_version_screen_grid", rows=len(OLD_VERSIONS) + 1 + ) + screen.clear_grid(wid="select_old_version_screen_grid") + screen.fetch_releases(OLD_VERSIONS) + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + self.assertEqual(len(grid.children), len(OLD_VERSIONS) + 1) + + set_background_calls = [] + set_screen_calls = [] + + for button in grid.children: + on_release = getattr(screen, f"on_release_{button.id}") + on_release(button) + set_background_calls.append(call(wid=button.id, rgba=(0, 0, 0, 1))) + + if button.id == "select_old_version_back": + set_screen_calls.append( + call(name="SelectVersionScreen", direction="right") + ) + else: + set_screen_calls.append(call(name="MainScreen", direction="right")) + + mock_set_background.assert_has_calls(set_background_calls) + mock_set_screen.assert_has_calls(set_screen_calls) + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_locale(self, mock_get_locale): + screen = SelectOldVersionScreen() + screen.make_grid( + wid="select_old_version_screen_grid", rows=len(OLD_VERSIONS) + 1 + ) + screen.clear_grid(wid="select_old_version_screen_grid") + screen.fetch_releases(OLD_VERSIONS) + self.render(screen) + + screen.update(name="ConfigKruxInstaller", key="locale", value="pt_BR.UTF-8") + text_back = "Voltar" + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + button_back = grid.children[0] + + self.assertEqual(button_back.text, text_back) + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update_locale(self, mock_redirect_error, mock_get_locale): + screen = SelectOldVersionScreen() + screen.make_grid( + wid="select_old_version_screen_grid", rows=len(OLD_VERSIONS) + 1 + ) + screen.clear_grid(wid="select_old_version_screen_grid") + screen.fetch_releases(OLD_VERSIONS) + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + screen.update(name="Mock", key="locale", value="pt_BR.UTF-8") + + mock_redirect_error.assert_called_once_with("Invalid screen name: Mock") + mock_get_locale.assert_any_call() diff --git a/e2e/test_007_warning_beta_screen.py b/e2e/test_007_warning_beta_screen.py new file mode 100644 index 00000000..d1eac310 --- /dev/null +++ b/e2e/test_007_warning_beta_screen.py @@ -0,0 +1,205 @@ +import os +import sys +from unittest.mock import patch +from kivy.base import EventLoop, EventLoopBase +from kivy.tests.common import GraphicUnitTest +from kivy.core.text import LabelBase, DEFAULT_FONT +from src.app.screens.warning_beta_screen import WarningBetaScreen + + +class TestSelectVersionScreen(GraphicUnitTest): + + @classmethod + def setUpClass(cls): + cwd_path = os.path.dirname(__file__) + rel_assets_path = os.path.join(cwd_path, "..", "assets") + assets_path = os.path.abspath(rel_assets_path) + noto_sans_path = os.path.join(assets_path, "NotoSansCJK_Cy_SC_KR_Krux.ttf") + LabelBase.register(DEFAULT_FONT, noto_sans_path) + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_render_main_screen(self, mock_get_locale): + screen = WarningBetaScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + button = grid.children[0] + sizes = [screen.SIZE_MM, screen.SIZE_M, screen.SIZE_MP] + + self.assertEqual(window.children[0], screen) + self.assertEqual(screen.name, "WarningBetaScreen") + self.assertEqual(screen.id, "warning_beta_screen") + self.assertEqual(grid.id, "warning_beta_screen_grid") + self.assertEqual(button.id, "warning_beta_screen_warn") + + text = "".join( + [ + f"[size={sizes[0]}sp]", + "[color=#efcc00]", + "[b]WARNING[/b]", + "[/color]", + "[/size]", + "\n", + "\n", + f"[size={sizes[1]}sp]", + "[color=#efcc00]This is our test repository[/color]", + "[/size]", + "\n", + f"[size={sizes[2]}sp]These are unsigned binaries for the latest and most experimental features[/size]", + "\n", + f"[size={sizes[2]}sp]and it's just for trying new things and providing feedback.[/size]", + "\n", + "\n", + f"[size={sizes[0]}sp]", + "[color=#00ff00]", + "[u]Proceed[/u]", + "[/color]", + " ", + "[color=#ff0000]", + "[u]Back[/u]", + "[/color]", + "[/size]", + ] + ) + + self.assertEqual(button.text, text) + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.warning_beta_screen.WarningBetaScreen.set_background") + def test_on_press(self, mock_set_background, mock_get_locale): + screen = WarningBetaScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + button = grid.children[0] + + action = getattr(screen, "on_press_warning_beta_screen_warn") + action(button) + + mock_set_background.assert_called_once_with( + wid=button.id, rgba=(0.25, 0.25, 0.25, 1) + ) + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.warning_beta_screen.WarningBetaScreen.set_background") + @patch("src.app.screens.warning_beta_screen.WarningBetaScreen.set_screen") + def test_on_release(self, mock_set_screen, mock_set_background, mock_get_locale): + screen = WarningBetaScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + button = grid.children[0] + + action = getattr(screen, "on_release_warning_beta_screen_warn") + action(button) + mock_set_background.assert_called_once_with(wid=button.id, rgba=(0, 0, 0, 1)) + mock_set_screen.assert_called_once_with(name="MainScreen", direction="right") + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_locale(self, mock_get_locale): + screen = WarningBetaScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + button = grid.children[0] + + fontsize_mm = 0 + fontsize_m = 0 + fontsize_mp = 0 + + if sys.platform in ("linux", "win32"): + fontsize_mm = window.size[0] // 24 + fontsize_m = window.size[0] // 32 + fontsize_mp = window.size[0] // 48 + + if sys.platform == "darwin": + fontsize_mm = window.size[0] // 48 + fontsize_m = window.size[0] // 64 + fontsize_mp = window.size[0] // 128 + + screen.update(name="ConfigKruxInstaller", key="locale", value="pt_BR.UTF-8") + text = "".join( + [ + f"[size={fontsize_mm}sp][color=#efcc00][b]ADVERTÊNCIA[/b][/color][/size]", + "\n", + "\n", + f"[size={fontsize_m}sp][color=#efcc00]Este é nosso repositório de testes[/color][/size]", + "\n", + f"[size={fontsize_mp}sp]Estes são binários não assinados dos recursos mais experimentais[/size]", + "\n", + f"[size={fontsize_mp}sp]e serve apenas para experimentar coisas novas e dar opiniões.[/size]", + "\n", + "\n", + f"[size={fontsize_mm}sp]", + "[color=#00ff00]Proceder[/color] [color=#ff0000]Voltar[/color]", + "[/size]", + ] + ) + + self.assertEqual(button.text, text) + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update_locale_wrong_name(self, mock_redirect_error, mock_get_locale): + screen = WarningBetaScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + screen.update(name="Mock", key="locale", value="pt_BR.UTF-8") + + mock_redirect_error.assert_called_once_with("Invalid screen name: Mock") + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update_locale_wrong_key(self, mock_redirect_error, mock_get_locale): + screen = WarningBetaScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + screen.update(name="ConfigKruxInstaller", key="mock") + + mock_redirect_error.assert_called_once_with('Invalid key: "mock"') + mock_get_locale.assert_any_call() diff --git a/e2e/test_008_about_screen.py b/e2e/test_008_about_screen.py new file mode 100644 index 00000000..0e21c35c --- /dev/null +++ b/e2e/test_008_about_screen.py @@ -0,0 +1,179 @@ +import os +from unittest.mock import patch, MagicMock +from kivy.base import EventLoop, EventLoopBase +from kivy.tests.common import GraphicUnitTest +from kivy.core.text import LabelBase, DEFAULT_FONT +from src.app.screens.about_screen import AboutScreen + + +class TestAboutScreen(GraphicUnitTest): + + @classmethod + def setUpClass(cls): + cwd_path = os.path.dirname(__file__) + rel_assets_path = os.path.join(cwd_path, "..", "assets") + assets_path = os.path.abspath(rel_assets_path) + noto_sans_path = os.path.join(assets_path, "NotoSansCJK_Cy_SC_KR_Krux.ttf") + LabelBase.register(DEFAULT_FONT, noto_sans_path) + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_render_main_screen(self, mock_get_locale): + screen = AboutScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + label = grid.children[0] + + self.assertEqual(window.children[0], screen) + self.assertEqual(screen.name, "AboutScreen") + self.assertEqual(screen.id, "about_screen") + self.assertEqual(grid.id, "about_screen_grid") + self.assertEqual(label.id, "about_screen_label") + + text = "".join( + [ + f"[size={screen.SIZE_G}sp]", + "[ref=SourceCode][b]v0.0.2-alpha[/b][/ref]", + "[/size]", + "\n", + "\n", + f"[size={screen.SIZE_M}sp]", + "follow us on X: ", + "[color=#00AABB]", + "[ref=X][u]@selfcustodykrux[/u][/ref]", + "[/color]", + "[/size]", + "\n", + "\n", + f"[size={screen.SIZE_M}sp]", + "[color=#00FF00]", + "[ref=Back]", + "[u]Back[/u]", + "[/ref]", + "[/color]", + "[/size]", + ] + ) + + self.assertEqual(label.text, text) + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="pt_BR.UTF-8" + ) + def test_update_locale(self, mock_get_locale): + screen = AboutScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + label = grid.children[0] + + screen.update(name="ConfigKruxInstaller", key="locale", value="pt_BR.UTF-8") + + text = "".join( + [ + f"[size={screen.SIZE_G}sp]", + "[ref=SourceCode][b]v0.0.2-alpha[/b][/ref]", + "[/size]", + "\n", + "\n", + f"[size={screen.SIZE_M}sp]", + "siga-nos no X: ", + "[color=#00AABB]", + "[ref=X][u]@selfcustodykrux[/u][/ref]", + "[/color]", + "[/size]", + "\n", + "\n", + f"[size={screen.SIZE_M}sp]", + "[color=#00FF00]", + "[ref=Back]", + "[u]Voltar[/u]", + "[/ref]", + "[/color]", + "[/size]", + ] + ) + + print(text) + print("=========") + print(label.text) + + self.assertEqual(label.text, text) + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.about_screen.webbrowser") + def test_on_press_version(self, mock_webbrowser, mock_get_locale): + mock_webbrowser.open = MagicMock() + + screen = AboutScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + action = getattr(screen, "on_ref_press_about_screen_label") + action("Mock", "SourceCode") + + mock_get_locale.assert_any_call() + mock_webbrowser.open.assert_called_once_with( + "https://selfcustody.github.io/krux/getting-started/installing/from-gui/" + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.about_screen.webbrowser") + def test_on_press_x_formerly_known_as_twitter( + self, mock_webbrowser, mock_get_locale + ): + mock_webbrowser.open = MagicMock() + + screen = AboutScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + action = getattr(screen, "on_ref_press_about_screen_label") + action("Mock", "X") + + mock_get_locale.assert_any_call() + mock_webbrowser.open.assert_called_once_with("https://x.com/selfcustodykrux") + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.about_screen.AboutScreen.set_screen") + def test_on_press_back(self, mock_set_screen, mock_get_locale): + screen = AboutScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + action = getattr(screen, "on_ref_press_about_screen_label") + action("Mock", "Back") + + mock_get_locale.assert_any_call() + mock_set_screen.assert_called_once_with(name="MainScreen", direction="right") diff --git a/e2e/test_009_base_download_screen.py b/e2e/test_009_base_download_screen.py new file mode 100644 index 00000000..32658c9b --- /dev/null +++ b/e2e/test_009_base_download_screen.py @@ -0,0 +1,169 @@ +import os +from unittest.mock import patch, MagicMock +from kivy.base import EventLoop, EventLoopBase +from kivy.tests.common import GraphicUnitTest +from kivy.core.text import LabelBase, DEFAULT_FONT +from src.app.screens.base_download_screen import BaseDownloadScreen + + +class TestBaseDownloadScreen(GraphicUnitTest): + + @classmethod + def setUpClass(cls): + cwd_path = os.path.dirname(__file__) + rel_assets_path = os.path.join(cwd_path, "..", "assets") + assets_path = os.path.abspath(rel_assets_path) + noto_sans_path = os.path.join(assets_path, "NotoSansCJK_Cy_SC_KR_Krux.ttf") + LabelBase.register(DEFAULT_FONT, noto_sans_path) + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_init( + self, + mock_get_locale, + ): + screen = BaseDownloadScreen(wid="mock_screen", name="MockScreen") + screen.to_screen = "AnotherMockScreen" + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + # default assertions + self.assertEqual(screen.downloader, None) + self.assertEqual(screen.thread, None) + self.assertEqual(screen.trigger, None) + self.assertEqual(screen.version, None) + self.assertEqual(screen.to_screen, "AnotherMockScreen") + self.assertEqual(grid.id, "mock_screen_grid") + self.assertEqual(grid.children[1].id, "mock_screen_progress") + self.assertEqual(grid.children[0].id, "mock_screen_info") + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_download_screen.Clock.create_trigger") + def test_set_trigger(self, mock_create_trigger, mock_get_locale): + mock_trigger = MagicMock() + + screen = BaseDownloadScreen(wid="mock_screen", name="MockScreen") + screen.to_screen = "AnotherMockScreen" + screen.trigger = mock_trigger + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # default assertions + self.assertFalse(screen.trigger is None) + + # patch assertions + mock_create_trigger.assert_called() + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_set_thread(self, mock_get_locale): + mock_target = MagicMock() + + screen = BaseDownloadScreen(wid="mock_screen", name="MockScreen") + screen.to_screen = "AnotherMockScreen" + screen.thread = MagicMock(name="mock", target=mock_target) + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # default assertions + self.assertFalse(screen.thread is None) + + # patch tests + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_on_enter(self, mock_redirect_error, mock_get_locale): + + screen = BaseDownloadScreen(wid="mock_screen", name="MockScreen") + screen.to_screen = "AnotherMockScreen" + screen.downloader = None + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.on_enter() + + # default assertions + self.assertTrue(screen.trigger is None) + self.assertTrue(screen.thread is None) + + # patch tests + mock_redirect_error.assert_called_once_with( + "Downloader isnt configured. Use `update` method first" + ) + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_download_screen.partial") + @patch("src.app.screens.base_download_screen.Clock.create_trigger") + @patch("src.app.screens.base_download_screen.Thread.start") + def test_on_enter( + self, + mock_thread, + mock_create_trigger, + mock_partial, + mock_get_locale, + ): + screen = BaseDownloadScreen(wid="mock_screen", name="MockScreen") + screen.to_screen = "AnotherMockScreen" + + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + setattr(BaseDownloadScreen, "on_trigger", MagicMock()) + # pylint: disable=no-member + BaseDownloadScreen.on_trigger.return_value = True + + setattr(BaseDownloadScreen, "on_progress", MagicMock()) + + # pylint: disable=no-member + BaseDownloadScreen.on_progress.return_value = True + screen.downloader = MagicMock() + + screen.on_enter() + + # patch tests + mock_get_locale.assert_any_call() + + on_progress = getattr(BaseDownloadScreen, "on_progress") + mock_partial.assert_called_once_with( + screen.downloader.download, on_data=on_progress + ) + mock_create_trigger.assert_called() + mock_thread.assert_called_once() + # (name=screen.name, target=mock_partial()) diff --git a/e2e/test_010_download_stable_zip_screen.py b/e2e/test_010_download_stable_zip_screen.py new file mode 100644 index 00000000..925b279f --- /dev/null +++ b/e2e/test_010_download_stable_zip_screen.py @@ -0,0 +1,372 @@ +import os +import sys +from unittest.mock import patch, call, MagicMock +from kivy.base import EventLoop, EventLoopBase +from kivy.tests.common import GraphicUnitTest +from kivy.core.text import LabelBase, DEFAULT_FONT +from src.app.screens.download_stable_zip_screen import DownloadStableZipScreen + + +class TestDownloadStableZipScreen(GraphicUnitTest): + + @classmethod + def setUpClass(cls): + cwd_path = os.path.dirname(__file__) + rel_assets_path = os.path.join(cwd_path, "..", "assets") + assets_path = os.path.abspath(rel_assets_path) + noto_sans_path = os.path.join(assets_path, "NotoSansCJK_Cy_SC_KR_Krux.ttf") + LabelBase.register(DEFAULT_FONT, noto_sans_path) + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_init(self, mock_get_locale): + screen = DownloadStableZipScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + # default assertions + self.assertEqual(screen.downloader, None) + self.assertEqual(screen.thread, None) + self.assertEqual(screen.trigger, None) + self.assertEqual(screen.version, None) + self.assertEqual(screen.to_screen, "DownloadStableZipSha256Screen") + self.assertEqual(grid.id, "download_stable_zip_screen_grid") + self.assertEqual(grid.children[1].id, "download_stable_zip_screen_progress") + self.assertEqual(grid.children[0].id, "download_stable_zip_screen_info") + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update_invalid_name( + self, + mock_redirect_error, + mock_get_locale, + ): + screen = DownloadStableZipScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name="MockScreen") + + # patch assertions + mock_get_locale.assert_any_call() + mock_redirect_error.assert_called_once_with("Invalid screen name: MockScreen") + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update_key(self, mock_redirect_error, mock_get_locale): + screen = DownloadStableZipScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name="ConfigKruxInstaller", key="mock") + + # patch assertions + mock_get_locale.assert_any_call() + mock_redirect_error.assert_called_once_with('Invalid key: "mock"') + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_locale(self, mock_get_locale): + screen = DownloadStableZipScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name="ConfigKruxInstaller", key="locale", value="en_US.UTF-8") + + # default assertions + self.assertEqual(screen.locale, "en_US.UTF-8") + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", + return_value="mockdir", + ) + @patch("src.app.screens.download_stable_zip_screen.ZipDownloader") + def test_update_version( + self, + mock_downloader, + mock_get_destdir_assets, + mock_get_locale, + ): + + screen = DownloadStableZipScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + + screen.update(name="ConfigKruxInstaller", key="version", value="v0.0.1") + + # patch assertions + mock_get_locale.assert_any_call() + mock_get_destdir_assets.assert_any_call() + mock_downloader.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_progress(self, mock_get_locale): + screen = DownloadStableZipScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + fontsize_g = 0 + fontsize_mp = 0 + + if sys.platform in ("linux", "win32"): + fontsize_g = window.size[0] // 16 + fontsize_mp = window.size[0] // 48 + + if sys.platform == "darwin": + fontsize_g = window.size[0] // 32 + fontsize_mp = window.size[0] // 128 + + # do tests + screen.update( + name="ConfigKruxInstaller", + key="progress", + value={"downloaded_len": 210000, "content_len": 21000000}, + ) + + # do tests + text = "".join( + [ + f"[size={fontsize_g}sp][b]1.00 %[/b][/size]", + "\n", + f"[size={fontsize_mp}sp]", + "0.20", + " of ", + "20.03 MB", + "[/size]", + ] + ) + + self.assertEqual(screen.ids["download_stable_zip_screen_progress"].text, text) + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_progress_100_percent(self, mock_get_locale): + screen = DownloadStableZipScreen() + screen.version = "v24.07.0" + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + fontsize_g = 0 + fontsize_mp = 0 + + if sys.platform in ("linux", "win32"): + fontsize_g = window.size[0] // 16 + fontsize_mp = window.size[0] // 48 + + if sys.platform == "darwin": + fontsize_g = window.size[0] // 32 + fontsize_mp = window.size[0] // 128 + + # do tests + with patch.object(screen, "trigger") as mock_trigger, patch.object( + screen, "downloader" + ) as mock_downloader: + + mock_downloader.destdir = "mockdir" + screen.update( + name="ConfigKruxInstaller", + key="progress", + value={"downloaded_len": 21000000, "content_len": 21000000}, + ) + + # do tests + text_progress = "".join( + [ + f"[size={fontsize_g}sp][b]100.00 %[/b][/size]", + "\n", + f"[size={fontsize_mp}sp]", + "20.03", + " of ", + "20.03", + " MB", + "[/size]", + ] + ) + + filepath = os.path.join("mockdir", "krux-v24.07.0.zip") + text_info = "".join( + [ + f"[size={fontsize_mp}sp]", + filepath, + "\n", + "downloaded", + "[/size]", + ] + ) + + self.assertEqual( + screen.ids["download_stable_zip_screen_progress"].text, text_progress + ) + self.assertEqual( + screen.ids["download_stable_zip_screen_info"].text, text_info + ) + + # patch assertions + mock_get_locale.assert_any_call() + assert len(mock_trigger.mock_calls) >= 1 + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.download_stable_zip_screen.ZipDownloader") + @patch("src.app.screens.download_stable_zip_screen.partial") + @patch("src.app.screens.download_stable_zip_screen.Clock.schedule_once") + def test_on_progress( + self, + mock_schedule_once, + mock_partial, + mock_downloader, + mock_get_locale, + ): + + mock_downloader.downloaded_len = 8 + mock_downloader.content_len = 21000000 + + # screen + screen = DownloadStableZipScreen() + screen.downloader = mock_downloader + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # default assertions + # pylint: disable=no-member + DownloadStableZipScreen.on_progress(data=b"") + + # patch assertions + mock_get_locale.assert_any_call() + mock_partial.assert_has_calls( + [ + call(screen.update, name=screen.name, key="canvas"), + call( + screen.update, + name=screen.name, + key="progress", + value={"downloaded_len": 8, "content_len": 21000000}, + ), + ] + ) + mock_schedule_once.assert_has_calls( + [call(mock_partial(), 0), call(mock_partial(), 0)] + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.download_stable_zip_screen.time.sleep") + @patch("src.app.screens.download_stable_zip_screen.DownloadStableZipScreen.manager") + @patch("src.app.screens.download_stable_zip_screen.partial") + @patch("src.app.screens.download_stable_zip_screen.Clock.schedule_once") + @patch( + "src.app.screens.download_stable_zip_screen.DownloadStableZipScreen.set_screen" + ) + def test_on_trigger( + self, + mock_set_screen, + mock_schedule_once, + mock_partial, + mock_manager, + mock_sleep, + mock_get_locale, + ): + # Mocks + mock_manager.get_screen = MagicMock() + + # screen + screen = DownloadStableZipScreen() + screen.version = "v0.0.1" + + # pylint: disable=no-member + screen.trigger = screen.on_trigger + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + # pylint: disable=no-member + DownloadStableZipScreen.on_trigger(0) + + # default assertions + self.assertFalse(screen.on_trigger is None) + self.assertFalse(screen.trigger is None) + + # patch assertions + mock_get_locale.assert_any_call() + mock_sleep.assert_called_once_with(2.1) + mock_manager.get_screen.assert_called_once_with("DownloadStableZipSha256Screen") + mock_partial.assert_has_calls( + [ + call(screen.update, name=screen.name, key="canvas"), + call( + mock_manager.get_screen().update, + name=screen.name, + key="version", + value="v0.0.1", + ), + ] + ) + mock_schedule_once.assert_has_calls( + [call(mock_partial(), 0), call(mock_partial(), 0)] + ) + mock_set_screen.assert_called_once_with( + name="DownloadStableZipSha256Screen", direction="left" + ) diff --git a/e2e/test_011_download_stable_zip_sha256_screen.py b/e2e/test_011_download_stable_zip_sha256_screen.py new file mode 100644 index 00000000..a43cbdb3 --- /dev/null +++ b/e2e/test_011_download_stable_zip_sha256_screen.py @@ -0,0 +1,385 @@ +import os +import sys +from unittest.mock import patch, call, MagicMock +from kivy.base import EventLoop, EventLoopBase +from kivy.tests.common import GraphicUnitTest +from kivy.core.text import LabelBase, DEFAULT_FONT +from src.app.screens.download_stable_zip_sha256_screen import ( + DownloadStableZipSha256Screen, +) + + +class TestDownloadStableZipSha256Screen(GraphicUnitTest): + + @classmethod + def setUpClass(cls): + cwd_path = os.path.dirname(__file__) + rel_assets_path = os.path.join(cwd_path, "..", "assets") + assets_path = os.path.abspath(rel_assets_path) + noto_sans_path = os.path.join(assets_path, "NotoSansCJK_Cy_SC_KR_Krux.ttf") + LabelBase.register(DEFAULT_FONT, noto_sans_path) + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_init(self, mock_get_locale): + screen = DownloadStableZipSha256Screen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + # default assertions + self.assertEqual(screen.downloader, None) + self.assertEqual(screen.thread, None) + self.assertEqual(screen.trigger, None) + self.assertEqual(screen.version, None) + self.assertEqual(screen.to_screen, "DownloadStableZipSigScreen") + self.assertEqual(grid.id, "download_stable_zip_sha256_screen_grid") + self.assertEqual( + grid.children[1].id, "download_stable_zip_sha256_screen_progress" + ) + self.assertEqual(grid.children[0].id, "download_stable_zip_sha256_screen_info") + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update_invalid_name(self, mock_redirect_error, mock_get_locale): + screen = DownloadStableZipSha256Screen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name="MockScreen") + + # default assertions + mock_redirect_error.assert_called_once_with("Invalid screen name: MockScreen") + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update_key(self, mock_redirect_error, mock_get_locale): + screen = DownloadStableZipSha256Screen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name="DownloadStableZipScreen", key="mock") + + # default assertions + mock_redirect_error.assert_any_call('Invalid key: "mock"') + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_locale(self, mock_get_locale): + screen = DownloadStableZipSha256Screen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name="ConfigKruxInstaller", key="locale", value="en_US.UTF-8") + + # default assertions + self.assertEqual(screen.locale, "en_US.UTF-8") + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", + return_value="mockdir", + ) + @patch("src.app.screens.download_stable_zip_sha256_screen.Sha256Downloader") + def test_update_version( + self, + mock_downloader, + mock_get_destdir_assets, + mock_get_locale, + ): + screen = DownloadStableZipSha256Screen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name="DownloadStableZipScreen", key="version", value="v0.0.1") + + # patch assertions + mock_get_locale.assert_any_call() + mock_downloader.assert_called_once() + mock_get_destdir_assets.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_progress(self, mock_get_locale): + screen = DownloadStableZipSha256Screen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + fontsize_g = 0 + fontsize_mp = 0 + + if sys.platform in ("linux", "win32"): + fontsize_g = window.size[0] // 16 + fontsize_mp = window.size[0] // 48 + + if sys.platform == "darwin": + fontsize_g = window.size[0] // 32 + fontsize_mp = window.size[0] // 128 + + # do tests + + screen.update( + name="ConfigKruxInstaller", + key="progress", + value={"downloaded_len": 210000, "content_len": 21000000}, + ) + + # do tests + text = "".join( + [ + f"[size={fontsize_g}sp][b]1.00 %[/b][/size]", + "\n", + f"[size={fontsize_mp}sp]", + "210000", + " of ", + "21000000", + " B", + "[/size]", + ] + ) + + self.assertEqual( + screen.ids["download_stable_zip_sha256_screen_progress"].text, text + ) + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.download_stable_zip_sha256_screen.Sha256Downloader") + @patch("src.app.screens.download_stable_zip_sha256_screen.partial") + @patch("src.app.screens.download_stable_zip_sha256_screen.Clock.schedule_once") + def test_on_progress( + self, + mock_schedule_once, + mock_partial, + mock_downloader, + mock_get_locale, + ): + + mock_downloader.downloaded_len = 8 + mock_downloader.content_len = 21000000 + + # screen + screen = DownloadStableZipSha256Screen() + screen.downloader = mock_downloader + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # default assertions + # pylint: disable=no-member + DownloadStableZipSha256Screen.on_progress(data=b"") + + # patch assertions + mock_get_locale.assert_any_call() + mock_partial.assert_has_calls( + [ + call(screen.update, name=screen.name, key="canvas"), + call( + screen.update, + name=screen.name, + key="progress", + value={"downloaded_len": 8, "content_len": 21000000}, + ), + ] + ) + + mock_schedule_once.assert_has_calls( + [call(mock_partial(), 0), call(mock_partial(), 0)] + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_progress_100_percent( + self, + mock_get_locale, + ): + screen = DownloadStableZipSha256Screen() + screen.version = "v24.07.0" + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + fontsize_g = 0 + fontsize_mp = 0 + + if sys.platform in ("linux", "win32"): + fontsize_g = window.size[0] // 16 + fontsize_mp = window.size[0] // 48 + + if sys.platform == "darwin": + fontsize_g = window.size[0] // 32 + fontsize_mp = window.size[0] // 128 + + # do tests + with patch.object(screen, "trigger") as mock_trigger, patch.object( + screen, "downloader" + ) as mock_downloader: + + mock_downloader.destdir = "mockdir" + screen.update( + name="ConfigKruxInstaller", + key="progress", + value={"downloaded_len": 21, "content_len": 21}, + ) + + # do tests + text_progress = "".join( + [ + f"[size={fontsize_g}sp][b]100.00 %[/b][/size]", + "\n", + f"[size={fontsize_mp}sp]", + "21", + " of ", + "21", + " B", + "[/size]", + ] + ) + + filepath = os.path.join("mockdir", "krux-v24.07.0.zip.sha256.txt") + text_info = "".join( + [ + f"[size={fontsize_mp}sp]", + filepath, + "\n", + "downloaded", + "[/size]", + ] + ) + + self.assertEqual( + screen.ids["download_stable_zip_sha256_screen_progress"].text, + text_progress, + ) + self.assertEqual( + screen.ids["download_stable_zip_sha256_screen_info"].text, text_info + ) + + # patch assertions + mock_get_locale.assert_any_call() + assert len(mock_trigger.mock_calls) >= 1 + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.download_stable_zip_sha256_screen.time.sleep") + @patch( + "src.app.screens.download_stable_zip_sha256_screen.DownloadStableZipSha256Screen.manager" + ) + @patch("src.app.screens.download_stable_zip_sha256_screen.partial") + @patch("src.app.screens.download_stable_zip_sha256_screen.Clock.schedule_once") + @patch( + "src.app.screens.download_stable_zip_sha256_screen.DownloadStableZipSha256Screen.set_screen" + ) + def test_on_trigger( + self, + mock_set_screen, + mock_schedule_once, + mock_partial, + mock_manager, + mock_sleep, + mock_get_locale, + ): + # Mocks + mock_manager.get_screen = MagicMock() + + # screen + screen = DownloadStableZipSha256Screen() + screen.version = "v0.0.1" + + # pylint: disable=no-member + screen.trigger = screen.on_trigger + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + # pylint: disable=no-member + DownloadStableZipSha256Screen.on_trigger(0) + + # default assertions + self.assertFalse(screen.on_trigger is None) + self.assertFalse(screen.trigger is None) + + # patch assertions + mock_get_locale.assert_any_call() + mock_sleep.assert_called_once_with(2.1) + mock_manager.get_screen.assert_called_once_with("DownloadStableZipSigScreen") + mock_partial.assert_has_calls( + [ + call(screen.update, name=screen.name, key="canvas"), + call( + mock_manager.get_screen().update, + name=screen.name, + key="version", + value="v0.0.1", + ), + ] + ) + mock_schedule_once.assert_has_calls( + [call(mock_partial(), 0), call(mock_partial(), 0)] + ) + mock_set_screen.assert_called_once_with( + name="DownloadStableZipSigScreen", direction="left" + ) diff --git a/e2e/test_012_download_stable_zip_sig_screen.py b/e2e/test_012_download_stable_zip_sig_screen.py new file mode 100644 index 00000000..bbe5b76c --- /dev/null +++ b/e2e/test_012_download_stable_zip_sig_screen.py @@ -0,0 +1,372 @@ +import os +import sys +from unittest.mock import patch, call, MagicMock +from kivy.base import EventLoop, EventLoopBase +from kivy.tests.common import GraphicUnitTest +from kivy.core.text import LabelBase, DEFAULT_FONT +from src.app.screens.download_stable_zip_sig_screen import ( + DownloadStableZipSigScreen, +) + + +class TestDownloadStableZipSigScreen(GraphicUnitTest): + + @classmethod + def setUpClass(cls): + cwd_path = os.path.dirname(__file__) + rel_assets_path = os.path.join(cwd_path, "..", "assets") + assets_path = os.path.abspath(rel_assets_path) + noto_sans_path = os.path.join(assets_path, "NotoSansCJK_Cy_SC_KR_Krux.ttf") + LabelBase.register(DEFAULT_FONT, noto_sans_path) + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_init(self, mock_get_locale): + screen = DownloadStableZipSigScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + # default assertions + self.assertEqual(screen.downloader, None) + self.assertEqual(screen.thread, None) + self.assertEqual(screen.trigger, None) + self.assertEqual(screen.version, None) + self.assertEqual(screen.to_screen, "DownloadSelfcustodyPemScreen") + self.assertEqual(grid.id, "download_stable_zip_sig_screen_grid") + self.assertEqual(grid.children[1].id, "download_stable_zip_sig_screen_progress") + self.assertEqual(grid.children[0].id, "download_stable_zip_sig_screen_info") + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update_invalid_name(self, mock_redirect_error, mock_get_locale): + screen = DownloadStableZipSigScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name="MockScreen") + + # patch assertions + mock_redirect_error.assert_called_once_with("Invalid screen name: MockScreen") + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update_key(self, mock_redirect_error, mock_get_locale): + screen = DownloadStableZipSigScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name=screen.name, key="mock") + + # patch assertions + mock_redirect_error.assert_called_once_with('Invalid key: "mock"') + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_locale(self, mock_get_locale): + screen = DownloadStableZipSigScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name="ConfigKruxInstaller", key="locale", value="en_US.UTF-8") + + # default assertions + self.assertEqual(screen.locale, "en_US.UTF-8") + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", + return_value="mockdir", + ) + @patch("src.app.screens.download_stable_zip_sig_screen.SigDownloader") + def test_update_version( + self, mock_downloader, mock_destdir_assets, mock_get_locale + ): + screen = DownloadStableZipSigScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name=screen.name, key="version", value="v0.0.1") + + # patch assertions + mock_get_locale.assert_any_call() + mock_destdir_assets.assert_any_call() + mock_downloader.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_progress(self, mock_get_locale): + screen = DownloadStableZipSigScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + fontsize_g = 0 + fontsize_mp = 0 + + if sys.platform in ("linux", "win32"): + fontsize_g = window.size[0] // 16 + fontsize_mp = window.size[0] // 48 + + if sys.platform == "darwin": + fontsize_g = window.size[0] // 32 + fontsize_mp = window.size[0] // 128 + + # do tests + + screen.update( + name="ConfigKruxInstaller", + key="progress", + value={"downloaded_len": 210000, "content_len": 21000000}, + ) + + # do tests + text = "".join( + [ + f"[size={fontsize_g}sp][b]1.00 %[/b][/size]", + "\n", + f"[size={fontsize_mp}sp]", + "210000", + " of ", + "21000000", + " B", + "[/size]", + ] + ) + + self.assertEqual( + screen.ids["download_stable_zip_sig_screen_progress"].text, text + ) + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.download_stable_zip_sig_screen.SigDownloader") + @patch("src.app.screens.download_stable_zip_sig_screen.partial") + @patch("src.app.screens.download_stable_zip_sig_screen.Clock.schedule_once") + def test_on_progress( + self, + mock_schedule_once, + mock_partial, + mock_downloader, + mock_get_locale, + ): + mock_downloader.downloaded_len = 8 + mock_downloader.content_len = 21000000 + + # screen + screen = DownloadStableZipSigScreen() + screen.downloader = mock_downloader + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # default assertions + # pylint: disable=no-member + DownloadStableZipSigScreen.on_progress(data=b"") + + # patch assertions + mock_get_locale.assert_any_call() + mock_partial.assert_has_calls( + [ + call(screen.update, name=screen.name, key="canvas"), + call( + screen.update, + name=screen.name, + key="progress", + value={"downloaded_len": 8, "content_len": 21000000}, + ), + ] + ) + + mock_schedule_once.assert_has_calls( + [call(mock_partial(), 0), call(mock_partial(), 0)], any_order=True + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_progress_100_percent(self, mock_get_locale): + + screen = DownloadStableZipSigScreen() + screen.version = "v24.07.0" + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + fontsize_g = 0 + fontsize_mp = 0 + + if sys.platform in ("linux", "win32"): + fontsize_g = window.size[0] // 16 + fontsize_mp = window.size[0] // 48 + + if sys.platform == "darwin": + fontsize_g = window.size[0] // 32 + fontsize_mp = window.size[0] // 128 + + # do tests + with patch.object(screen, "trigger") as mock_trigger, patch.object( + screen, "downloader" + ) as mock_downloader: + + mock_downloader.destdir = "mockdir" + screen.update( + name="ConfigKruxInstaller", + key="progress", + value={"downloaded_len": 21, "content_len": 21}, + ) + + # do tests + text_progress = "".join( + [ + f"[size={fontsize_g}sp][b]100.00 %[/b][/size]", + "\n", + f"[size={fontsize_mp}sp]", + "21", + " of ", + "21", + " B", + "[/size]", + ] + ) + + filepath = os.path.join("mockdir", "krux-v24.07.0.zip.sig") + text_info = "".join( + [ + f"[size={fontsize_mp}sp]", + filepath, + "\n", + "downloaded", + "[/size]", + ] + ) + + self.assertEqual( + screen.ids["download_stable_zip_sig_screen_progress"].text, + text_progress, + ) + self.assertEqual( + screen.ids["download_stable_zip_sig_screen_info"].text, text_info + ) + + # patch assertions + mock_get_locale.assert_any_call() + assert len(mock_trigger.mock_calls) >= 1 + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.download_stable_zip_sig_screen.time.sleep") + @patch( + "src.app.screens.download_stable_zip_sig_screen.DownloadStableZipSigScreen.manager" + ) + @patch("src.app.screens.download_stable_zip_sig_screen.partial") + @patch("src.app.screens.download_stable_zip_sig_screen.Clock.schedule_once") + @patch( + "src.app.screens.download_stable_zip_sig_screen.DownloadStableZipSigScreen.set_screen" + ) + def test_on_trigger( + self, + mock_set_screen, + mock_schedule_once, + mock_partial, + mock_manager, + mock_sleep, + mock_get_locale, + ): + # Mocks + mock_manager.get_screen = MagicMock() + + # screen + screen = DownloadStableZipSigScreen() + screen.version = "v0.0.1" + + # pylint: disable=no-member + screen.trigger = screen.on_trigger + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + # pylint: disable=no-member + DownloadStableZipSigScreen.on_trigger(0) + + # default assertions + self.assertFalse(screen.on_trigger is None) + self.assertFalse(screen.trigger is None) + + # patch assertions + mock_get_locale.assert_any_call() + mock_sleep.assert_called_once_with(2.1) + mock_manager.get_screen.assert_called_once_with("DownloadSelfcustodyPemScreen") + mock_partial.assert_has_calls( + [ + call(screen.update, name=screen.name, key="canvas"), + call( + mock_manager.get_screen().update, + name=screen.name, + key="public-key-certificate", + ), + ] + ) + mock_schedule_once.assert_has_calls( + [call(mock_partial(), 0), call(mock_partial(), 0)], any_order=True + ) + mock_set_screen.assert_called_once_with( + name="DownloadSelfcustodyPemScreen", direction="left" + ) diff --git a/e2e/test_013_download_selfcustody_pem_screen.py b/e2e/test_013_download_selfcustody_pem_screen.py new file mode 100644 index 00000000..771e1eac --- /dev/null +++ b/e2e/test_013_download_selfcustody_pem_screen.py @@ -0,0 +1,339 @@ +import os +import sys +from unittest.mock import patch, call +from kivy.base import EventLoop, EventLoopBase +from kivy.tests.common import GraphicUnitTest +from kivy.core.text import LabelBase, DEFAULT_FONT +from src.app.screens.download_selfcustody_pem_screen import ( + DownloadSelfcustodyPemScreen, +) + + +class TestDownloadSelfcustodyPemScreen(GraphicUnitTest): + + @classmethod + def setUpClass(cls): + cwd_path = os.path.dirname(__file__) + rel_assets_path = os.path.join(cwd_path, "..", "assets") + assets_path = os.path.abspath(rel_assets_path) + noto_sans_path = os.path.join(assets_path, "NotoSansCJK_Cy_SC_KR_Krux.ttf") + LabelBase.register(DEFAULT_FONT, noto_sans_path) + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_init(self, mock_get_locale): + screen = DownloadSelfcustodyPemScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + # default assertions + self.assertEqual(screen.downloader, None) + self.assertEqual(screen.thread, None) + self.assertEqual(screen.trigger, None) + self.assertEqual(screen.to_screen, "VerifyStableZipScreen") + self.assertEqual(grid.id, "download_selfcustody_pem_screen_grid") + self.assertEqual( + grid.children[1].id, "download_selfcustody_pem_screen_progress" + ) + self.assertEqual(grid.children[0].id, "download_selfcustody_pem_screen_info") + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update_invalid_name(self, mock_redirect_error, mock_get_locale): + screen = DownloadSelfcustodyPemScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name="MockScreen") + + # patch assertions + mock_redirect_error.assert_called_once_with("Invalid screen name: MockScreen") + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update_key(self, mock_redirect_error, mock_get_locale): + screen = DownloadSelfcustodyPemScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name=screen.name, key="mock") + + # patch assertions + mock_redirect_error.assert_called_once_with('Invalid key: "mock"') + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_locale(self, mock_get_locale): + screen = DownloadSelfcustodyPemScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name="ConfigKruxInstaller", key="locale", value="en_US.UTF-8") + + # default assertions + self.assertEqual(screen.locale, "en_US.UTF-8") + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", + return_value="mockdir", + ) + @patch("src.app.screens.download_selfcustody_pem_screen.PemDownloader") + def test_update_public_key_certificate( + self, mock_downloader, mock_destdir_assets, mock_get_locale + ): + screen = DownloadSelfcustodyPemScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name=screen.name, key="public-key-certificate") + + # patch assertions + mock_get_locale.assert_any_call() + mock_destdir_assets.assert_any_call() + mock_downloader.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_progress(self, mock_get_locale): + screen = DownloadSelfcustodyPemScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + fontsize_g = 0 + fontsize_mp = 0 + + if sys.platform in ("linux", "win32"): + fontsize_g = window.size[0] // 16 + fontsize_mp = window.size[0] // 48 + + if sys.platform == "darwin": + fontsize_g = window.size[0] // 32 + fontsize_mp = window.size[0] // 128 + + # do tests + screen.update( + name="ConfigKruxInstaller", + key="progress", + value={"downloaded_len": 210000, "content_len": 21000000}, + ) + + # do tests + text = "".join( + [ + f"[size={fontsize_g}sp][b]1.00 %[/b][/size]", + "\n", + f"[size={fontsize_mp}sp]", + "210000", + " of ", + "21000000", + " B", + "[/size]", + ] + ) + + self.assertEqual( + screen.ids["download_selfcustody_pem_screen_progress"].text, text + ) + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.download_selfcustody_pem_screen.PemDownloader") + @patch("src.app.screens.download_selfcustody_pem_screen.partial") + @patch("src.app.screens.download_selfcustody_pem_screen.Clock.schedule_once") + def test_on_progress( + self, + mock_schedule_once, + mock_partial, + mock_downloader, + mock_get_locale, + ): + mock_downloader.downloaded_len = 8 + mock_downloader.content_len = 21000000 + + # screen + screen = DownloadSelfcustodyPemScreen() + screen.downloader = mock_downloader + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # default assertions + # pylint: disable=no-member + DownloadSelfcustodyPemScreen.on_progress(data=b"") + + # patch assertions + mock_get_locale.assert_any_call() + mock_partial.assert_has_calls( + [ + call(screen.update, name=screen.name, key="canvas"), + call( + screen.update, + name=screen.name, + key="progress", + value={"downloaded_len": 8, "content_len": 21000000}, + ), + ] + ) + + mock_schedule_once.assert_has_calls( + [call(mock_partial(), 0), call(mock_partial(), 0)], any_order=True + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_progress_100_percent(self, mock_get_locale): + screen = DownloadSelfcustodyPemScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + fontsize_g = 0 + fontsize_mp = 0 + + if sys.platform in ("linux", "win32"): + fontsize_g = window.size[0] // 16 + fontsize_mp = window.size[0] // 48 + + if sys.platform == "darwin": + fontsize_g = window.size[0] // 32 + fontsize_mp = window.size[0] // 128 + + # do tests + with patch.object(screen, "trigger") as mock_trigger, patch.object( + screen, "downloader" + ) as mock_downloader: + + mock_downloader.destdir = "mockdir" + screen.update( + name="ConfigKruxInstaller", + key="progress", + value={"downloaded_len": 210, "content_len": 21}, + ) + + # do tests + text_progress = "".join( + [ + f"[size={fontsize_g}sp][b]100.00 %[/b][/size]", + "\n", + f"[size={fontsize_mp}sp]", + "21", + " of ", + "21", + " B", + "[/size]", + ] + ) + + filepath = os.path.join("mockdir", "selfcustody.pem") + text_info = "".join( + [ + f"[size={fontsize_mp}sp]", + filepath, + "\n", + "downloaded", + "[/size]", + ] + ) + + self.assertEqual( + screen.ids["download_selfcustody_pem_screen_progress"].text, + text_progress, + ) + self.assertEqual( + screen.ids["download_selfcustody_pem_screen_info"].text, text_info + ) + + # patch assertions + mock_get_locale.assert_any_call() + assert len(mock_trigger.mock_calls) >= 1 + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.download_selfcustody_pem_screen.time.sleep") + @patch( + "src.app.screens.download_selfcustody_pem_screen.DownloadSelfcustodyPemScreen.set_screen" + ) + def test_on_trigger(self, mock_set_screen, mock_sleep, mock_get_locale): + # screen + screen = DownloadSelfcustodyPemScreen() + + # pylint: disable=no-member + screen.trigger = screen.on_trigger + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + # pylint: disable=no-member + DownloadSelfcustodyPemScreen.on_trigger(0) + + # default assertions + self.assertFalse(screen.on_trigger is None) + self.assertFalse(screen.trigger is None) + + # patch assertions + mock_get_locale.assert_any_call() + mock_sleep.assert_called_once_with(2.1) + mock_set_screen.assert_called_once_with( + name="VerifyStableZipScreen", direction="left" + ) diff --git a/e2e/test_014_download_beta_screen.py b/e2e/test_014_download_beta_screen.py new file mode 100644 index 00000000..daf0bbe7 --- /dev/null +++ b/e2e/test_014_download_beta_screen.py @@ -0,0 +1,481 @@ +import os +import sys +from unittest.mock import patch, call, MagicMock +from kivy.base import EventLoop, EventLoopBase +from kivy.tests.common import GraphicUnitTest +from kivy.core.text import LabelBase, DEFAULT_FONT +from src.app.screens.download_beta_screen import ( + DownloadBetaScreen, +) + + +class TestDownloadBetaScreen(GraphicUnitTest): + + @classmethod + def setUpClass(cls): + cwd_path = os.path.dirname(__file__) + rel_assets_path = os.path.join(cwd_path, "..", "assets") + assets_path = os.path.abspath(rel_assets_path) + noto_sans_path = os.path.join(assets_path, "NotoSansCJK_Cy_SC_KR_Krux.ttf") + LabelBase.register(DEFAULT_FONT, noto_sans_path) + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_init(self, mock_get_locale): + screen = DownloadBetaScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + # default assertions + self.assertEqual(screen.downloader, None) + self.assertEqual(screen.thread, None) + self.assertEqual(screen.trigger, None) + self.assertEqual(screen.firmware, None) + self.assertEqual(screen.device, None) + self.assertEqual(screen.to_screen, "FlashScreen") + self.assertEqual(grid.id, "download_beta_screen_grid") + self.assertEqual(grid.children[1].id, "download_beta_screen_progress") + self.assertEqual(grid.children[0].id, "download_beta_screen_info") + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update_invalid_name(self, mock_redirect_error, mock_get_locale): + screen = DownloadBetaScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name="MockScreen") + + # patch assertions + mock_redirect_error.assert_called_once_with("Invalid screen name: MockScreen") + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update_key(self, mock_redirect_error, mock_get_locale): + screen = DownloadBetaScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name=screen.name, key="mock") + + # patch assertions + mock_redirect_error.assert_called_once_with('Invalid key: "mock"') + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_locale(self, mock_get_locale): + screen = DownloadBetaScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name="ConfigKruxInstaller", key="locale", value="en_US.UTF-8") + + # default assertions + self.assertEqual(screen.locale, "en_US.UTF-8") + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update_firmware(self, mock_redirect_error, mock_get_locale): + screen = DownloadBetaScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name=screen.name, key="firmware", value="mock.kfpkg") + + # patch assertions + mock_redirect_error.assert_called_once_with("Invalid firmware: mock.kfpkg") + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_firmware(self, mock_get_locale): + screen = DownloadBetaScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name=screen.name, key="firmware", value="kboot.kfpkg") + + # default assertions + self.assertEqual(screen.firmware, "kboot.kfpkg") + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update_device(self, mock_redirect_error, mock_get_locale): + screen = DownloadBetaScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name=screen.name, key="device", value="mock") + + # default assertions + + # patch assertions + mock_redirect_error.assert_called_once_with('Invalid device: "mock"') + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_device(self, mock_get_locale): + screen = DownloadBetaScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name=screen.name, key="device", value="m5stickv") + + # default assertions + self.assertEqual(screen.device, "m5stickv") + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", + return_value="mockdir", + ) + def test_update_downloader(self, mock_destdir_assets, mock_get_locale): + screen = DownloadBetaScreen() + screen.firmware = "kboot.kfpkg" + screen.device = "amigo" + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + fontsize_mp = 0 + + if sys.platform in ("linux", "win32"): + fontsize_mp = window.size[0] // 48 + + if sys.platform == "darwin": + fontsize_mp = window.size[0] // 128 + + # do tests + screen.update(name=screen.name, key="downloader") + + firmware_path = os.path.join("mockdir", "krux_binaries", "maixpy_amigo") + + # do tests + text = "".join( + [ + f"[size={fontsize_mp}sp]", + "Downloading", + "\n", + "[color=#00AABB]", + "[ref=https://raw.githubusercontent.com/odudex/krux_binaries/main/maixpy_amigo/kboot.kfpkg]", + "https://raw.githubusercontent.com/odudex/krux_binaries/main/maixpy_amigo/kboot.kfpkg", + "[/ref]", + "[/color]", + "\n", + "to", + "\n", + firmware_path, + "\n", + "[/size]", + ] + ) + # default assertions + self.assertEqual(screen.ids["download_beta_screen_info"].text, text) + + # patch assertions + mock_get_locale.assert_any_call() + mock_destdir_assets.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_progress(self, mock_get_locale): + screen = DownloadBetaScreen() + screen.downloader = MagicMock(destdir="mockdir") + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + fontsize_g = 0 + fontsize_mp = 0 + + if sys.platform in ("linux", "win32"): + fontsize_g = window.size[0] // 16 + fontsize_mp = window.size[0] // 48 + + if sys.platform == "darwin": + fontsize_g = window.size[0] // 32 + fontsize_mp = window.size[0] // 128 + + # do tests + screen.update( + name="ConfigKruxInstaller", + key="progress", + value={"downloaded_len": 210000, "content_len": 21000000}, + ) + + # do tests + text = "".join( + [ + f"[size={fontsize_g}sp][b]1.00 %[/b][/size]", + "\n", + f"[size={fontsize_mp}sp]0.20 of 20.03 MB[/size]", + ] + ) + + self.assertEqual(screen.ids["download_beta_screen_progress"].text, text) + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_progress_100_percent(self, mock_get_locale): + screen = DownloadBetaScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + fontsize_g = 0 + fontsize_mp = 0 + + if sys.platform in ("linux", "win32"): + fontsize_g = window.size[0] // 16 + fontsize_mp = window.size[0] // 48 + + if sys.platform == "darwin": + fontsize_g = window.size[0] // 32 + fontsize_mp = window.size[0] // 128 + + # do tests + with patch.object(screen, "trigger") as mock_trigger, patch.object( + screen, "downloader" + ) as mock_downloader: + + mock_downloader.destdir = "mockdir" + screen.update( + name="ConfigKruxInstaller", + key="progress", + value={"downloaded_len": 21000000, "content_len": 21000000}, + ) + + # do tests + text_progress = "".join( + [ + f"[size={fontsize_g}sp][b]100.00 %[/b][/size]", + "\n", + f"[size={fontsize_mp}sp]20.03 of 20.03 MB[/size]", + ] + ) + + kboot = os.path.join("mockdir", "kboot.kfpkg") + text_info = "".join( + [f"[size={fontsize_mp}sp]", kboot, "\n", "downloaded", "[/size]"] + ) + + self.assertEqual( + screen.ids["download_beta_screen_progress"].text, text_progress + ) + self.assertEqual(screen.ids["download_beta_screen_info"].text, text_info) + + # patch assertions + mock_get_locale.assert_any_call() + assert len(mock_trigger.mock_calls) >= 1 + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.get_baudrate", return_value=1500000) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", + return_value="mockdir", + ) + @patch("src.app.screens.download_beta_screen.time.sleep") + @patch("src.app.screens.download_beta_screen.DownloadBetaScreen.manager") + @patch("src.app.screens.download_beta_screen.partial") + @patch("src.app.screens.download_beta_screen.Clock.schedule_once") + @patch("src.app.screens.download_beta_screen.DownloadBetaScreen.set_screen") + def test_on_trigger( + self, + mock_set_screen, + mock_schedule_once, + mock_partial, + mock_manager, + mock_sleep, + mock_get_destdir_assets, + mock_get_baudrate, + mock_get_locale, + ): + # Mocks + mock_manager.get_screen = MagicMock() + + # screen + screen = DownloadBetaScreen() + screen.baudrate = 1500000 + screen.device = "amigo" + screen.firmware = "kboot.kfpkg" + + # pylint: disable=no-member + screen.trigger = screen.on_trigger + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + # pylint: disable=no-member + DownloadBetaScreen.on_trigger(0) + + # default assertions + self.assertFalse(screen.on_trigger is None) + self.assertFalse(screen.trigger is None) + + # patch assertions + mock_get_locale.assert_any_call() + mock_get_destdir_assets.assert_any_call() + mock_get_baudrate.assert_any_call() + mock_sleep.assert_called_once_with(2.1) + mock_manager.get_screen.assert_called_once_with("FlashScreen") + + p = os.path.join( + "mockdir", + "krux_binaries", + "maixpy_amigo", + "kboot.kfpkg", + ) + mock_partial.assert_has_calls( + [ + call(screen.update, name=screen.name, key="canvas"), + call( + mock_manager.get_screen().update, + name=screen.name, + key="baudrate", + value=1500000, + ), + call( + mock_manager.get_screen().update, + name=screen.name, + key="firmware", + value=p, + ), + ] + ) + mock_schedule_once.assert_has_calls( + [call(mock_partial(), 0), call(mock_partial(), 0)] + ) + mock_set_screen.assert_called_once_with(name="FlashScreen", direction="left") + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.download_beta_screen.partial") + @patch("src.app.screens.download_beta_screen.Clock.schedule_once") + def test_on_progress(self, mock_schedule_once, mock_partial, mock_get_locale): + + # screen + screen = DownloadBetaScreen() + screen.baudrate = 1500000 + screen.device = "amigo" + screen.firmware = "kboot.kfpkg" + + # pylint: disable=no-member + screen.trigger = screen.on_trigger + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + # pylint: disable=no-member + with patch.object(screen, "downloader") as mock_downloader: + mock_downloader.downloaded_len = 21 + mock_downloader.content_len = 21000000 + DownloadBetaScreen.on_progress(data=[]) + + # default assertions + self.assertFalse(screen.on_progress is None) + + # patch assertions + mock_get_locale.assert_any_call() + mock_partial.assert_has_calls( + [ + call(screen.update, name=screen.name, key="canvas"), + call( + screen.update, + name=screen.name, + key="progress", + value={"downloaded_len": 21, "content_len": 21000000}, + ), + ] + ) + mock_schedule_once.assert_has_calls( + [call(mock_partial(), 0), call(mock_partial(), 0)], any_order=True + ) diff --git a/e2e/test_015_warning_already_downloaded_screen.py b/e2e/test_015_warning_already_downloaded_screen.py new file mode 100644 index 00000000..95124454 --- /dev/null +++ b/e2e/test_015_warning_already_downloaded_screen.py @@ -0,0 +1,223 @@ +import sys +from unittest.mock import patch, call, MagicMock +from kivy.base import EventLoop, EventLoopBase +from kivy.tests.common import GraphicUnitTest +from src.app.screens.warning_already_downloaded_screen import ( + WarningAlreadyDownloadedScreen, +) + + +class TestWarningAlreadyDownloadedScreen(GraphicUnitTest): + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_init(self, mock_get_locale): + screen = WarningAlreadyDownloadedScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # default assertions + self.assertTrue(f"{screen.id}_grid" in screen.ids) + self.assertTrue(f"{screen.id}_label" in screen.ids) + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update_invalid_name(self, mock_redirect_error, mock_get_locale): + screen = WarningAlreadyDownloadedScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name="MockScreen") + + # patch assertions + mock_redirect_error.assert_called_once_with("Invalid screen name: MockScreen") + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update_key(self, mock_redirect_error, mock_get_locale): + screen = WarningAlreadyDownloadedScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name=screen.name, key="mock") + + # patch assertions + mock_redirect_error.assert_called_once_with('Invalid key: "mock"') + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_locale(self, mock_get_locale): + screen = WarningAlreadyDownloadedScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name="ConfigKruxInstaller", key="locale", value="en_US.UTF-8") + + # default assertions + self.assertEqual(screen.locale, "en_US.UTF-8") + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_version(self, mock_get_locale): + attrs = {"get.side_effect": ["en-US.UTF8", "mockdir"]} + mock_get_locale.config = MagicMock() + mock_get_locale.config.configure_mock(**attrs) + + screen = WarningAlreadyDownloadedScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + size = [0, 0] + + if sys.platform in ("linux", "win32"): + size = [window.size[0] // 32, window.size[0] // 48, window.size[0] // 64] + + if sys.platform == "darwin": + size = [window.size[0] // 48, window.size[0] // 128, window.size[0] // 128] + + label_text = "".join( + [ + f"[size={size[0]}sp][b]Assets already downloaded[/b][/size]", + "\n", + f"[size={size[2]}sp]* krux-v0.0.1.zip[/size]", + "\n", + f"[size={size[2]}sp]* krux-v0.0.1.zip.sha256.txt[/size]", + "\n", + f"[size={size[2]}sp]* krux-v0.0.1.zip.sig[/size]", + "\n", + f"[size={size[2]}sp]* selfcustody.pem[/size]", + "\n", + "\n", + f"[size={size[1]}sp]Do you want to proceed with the same file or do you want to download it again?[/size]", + "\n", + "\n", + f"[size={size[0]}]" f"[color=#00ff00]", + "[ref=DownloadStableZipScreen]", + "[u]Download again[/u]", + "[/ref]", + "[/color]", + " ", + "[color=#efcc00]", + "[ref=VerifyStableZipScreen]", + "[u]Proceed with current file[/u]", + "[/ref]", + "[/color]", + "[/size]", + ] + ) + + screen.update(name=screen.name, key="version", value="v0.0.1") + + # default assertions + self.assertEqual(screen.ids[f"{screen.id}_label"].text, label_text) + + # patch assertions + mock_get_locale.assert_called() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.warning_already_downloaded_screen.WarningAlreadyDownloadedScreen.manager" + ) + @patch("src.app.screens.warning_already_downloaded_screen.partial") + @patch("src.app.screens.warning_already_downloaded_screen.Clock.schedule_once") + def test_on_press_donwload_button( + self, mock_schedule_once, mock_partial, mock_manager, mock_get_locale + ): + mock_get_locale.config = MagicMock() + mock_get_locale.config.get = MagicMock(return_value="en-US") + + mock_manager.get_screen = MagicMock() + + screen = WarningAlreadyDownloadedScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + action = getattr(WarningAlreadyDownloadedScreen, f"on_ref_press_{screen.id}") + action("Mock", "DownloadStableZipScreen") + + mock_get_locale.assert_any_call() + mock_manager.get_screen.assert_has_calls( + [call("MainScreen"), call("DownloadStableZipScreen")] + ) + mock_partial.assert_has_calls( + [ + call(screen.update, name=screen.name, key="canvas"), + call( + mock_manager.get_screen().update, + name=screen.name, + key="version", + value=mock_manager.get_screen().version, + ), + ] + ) + mock_schedule_once.assert_has_calls( + [call(mock_partial(), 0), call(mock_partial(), 0)] + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.warning_already_downloaded_screen.WarningAlreadyDownloadedScreen.set_screen" + ) + def test_on_press_proceed(self, mock_set_screen, mock_get_locale): + mock_get_locale.config = MagicMock() + mock_get_locale.config.get = MagicMock(return_value="en-US") + + screen = WarningAlreadyDownloadedScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + action = getattr(WarningAlreadyDownloadedScreen, f"on_ref_press_{screen.id}") + action("Mock", "VerifyStableZipScreen") + + mock_get_locale.assert_any_call() + mock_set_screen.assert_called_once_with( + name="VerifyStableZipScreen", direction="left" + ) diff --git a/e2e/test_016_unzip_stable_screen.py b/e2e/test_016_unzip_stable_screen.py new file mode 100644 index 00000000..55bba495 --- /dev/null +++ b/e2e/test_016_unzip_stable_screen.py @@ -0,0 +1,596 @@ +import os +import sys +from unittest.mock import patch, MagicMock +from kivy.base import EventLoop, EventLoopBase +from kivy.tests.common import GraphicUnitTest +from kivy.core.text import LabelBase, DEFAULT_FONT +from src.app.screens.unzip_stable_screen import ( + UnzipStableScreen, +) + + +class TestWarningAlreadyDownloadedScreen(GraphicUnitTest): + + @classmethod + def setUpClass(cls): + cwd_path = os.path.dirname(__file__) + rel_assets_path = os.path.join(cwd_path, "..", "assets") + assets_path = os.path.abspath(rel_assets_path) + noto_sans_path = os.path.join(assets_path, "NotoSansCJK_Cy_SC_KR_Krux.ttf") + LabelBase.register(DEFAULT_FONT, noto_sans_path) + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", return_value="mock" + ) + def test_init(self, mock_get_locale, mock_get_destdir_assets): + screen = UnzipStableScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + # default assertions + self.assertEqual(grid.id, f"{screen.id}_grid") + self.assertFalse( + hasattr(UnzipStableScreen, "on_press_unzip_stable_screen_button") + ) + self.assertFalse( + hasattr(UnzipStableScreen, "on_release_unzip_stable_screen_button") + ) + + # patch assertions + mock_get_destdir_assets.assert_any_call() + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", return_value="mock" + ) + def test_fail_update_invalid_name(self, mock_get_locale, mock_get_destdir_assets): + screen = UnzipStableScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + with self.assertRaises(ValueError) as exc_info: + screen.update(name="MockScreen") + + # default assertions + self.assertEqual(str(exc_info.exception), "Invalid screen name: MockScreen") + + # patch assertions + mock_get_destdir_assets.assert_any_call() + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", return_value="mock" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update_key( + self, + mock_redirect_error, + mock_get_destdir_assets, + mock_get_locale, + ): + screen = UnzipStableScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name=screen.name, key="mock") + + # default assertions + + # patch assertions + mock_get_destdir_assets.assert_any_call() + mock_get_locale.assert_any_call() + mock_redirect_error.assert_called_once_with('Invalid key: "mock"') + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", return_value="mock" + ) + def test_update_locale(self, mock_get_locale, mock_get_destdir_assets): + screen = UnzipStableScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name="ConfigKruxInstaller", key="locale", value="en_US.UTF-8") + + # default assertions + self.assertEqual(screen.locale, "en_US.UTF-8") + + # patch assertions + mock_get_destdir_assets.assert_any_call() + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", return_value="mock" + ) + def test_update_version(self, mock_get_locale, mock_get_destdir_assets): + screen = UnzipStableScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name="VerifyStableZipScreen", key="version", value="v0.0.1") + + # default assertions + self.assertEqual(screen.version, "v0.0.1") + + # patch assertions + mock_get_destdir_assets.assert_any_call() + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", return_value="mock" + ) + def test_update_device(self, mock_get_locale, mock_get_destdir_assets): + screen = UnzipStableScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name="VerifyStableZipScreen", key="device", value="mock") + + # default assertions + self.assertEqual(screen.device, "mock") + + # patch assertions + mock_get_destdir_assets.assert_any_call() + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", return_value="mock" + ) + def test_update_clear(self, mock_get_locale, mock_get_destdir_assets): + screen = UnzipStableScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + screen.make_button( + id=f"{screen.id}_mock_button", + root_widget=f"{screen.id}_grid", + text="Mock", + markup=True, + row=0, + on_press=MagicMock(), + on_release=MagicMock(), + ) + + # do tests + with patch.object( + screen.ids[f"{screen.id}_grid"], "clear_widgets" + ) as mock_clear: + screen.update(name="VerifyStableZipScreen", key="clear") + mock_clear.assert_called_once() + + # patch assertions + mock_get_destdir_assets.assert_any_call() + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", return_value="mock" + ) + def test_update_flash_button(self, mock_get_locale, mock_get_destdir_assets): + screen = UnzipStableScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + size = [0, 0] + + if sys.platform in ("linux", "win32"): + size = [window.size[0] // 24, window.size[0] // 48] + + if sys.platform == "darwin": + size = [window.size[0] // 48, window.size[0] // 128] + + # do tests + screen.update(name="VerifyStableZipScreen", key="device", value="mock") + screen.update(name="VerifyStableZipScreen", key="version", value="v0.0.1") + screen.update(name="VerifyStableZipScreen", key="flash-button") + + p = os.path.join("mock", "krux-v0.0.1", "maixpy_mock", "kboot.kfpkg") + text = "".join( + [ + f"[size={size[0]}sp]", + "Flash with", + "[/size]", + "\n", + f"[size={size[1]}sp]", + "[color=#efcc00]", + p, + "[/color]", + "[/size]", + ] + ) + + # default assertions + self.assertEqual(screen.ids[f"{screen.id}_flash_button"].text, text) + + # patch assertions + mock_get_destdir_assets.assert_any_call() + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", return_value="mock" + ) + def test_update_airgap_button(self, mock_get_locale, mock_get_destdir_assets): + screen = UnzipStableScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + size = [0, 0] + + if sys.platform in ("linux", "win32"): + size = [window.size[0] // 24, window.size[0] // 48] + + if sys.platform == "darwin": + size = [window.size[0] // 48, window.size[0] // 128] + + # do tests + screen.update(name="VerifyStableZipScreen", key="device", value="mock") + screen.update(name="VerifyStableZipScreen", key="version", value="v0.0.1") + screen.update(name="VerifyStableZipScreen", key="airgap-button") + + p = os.path.join("mock", "krux-v0.0.1", "maixpy_mock", "firmware.bin") + text = "".join( + [ + f"[size={size[0]}sp]", + "[color=#333333]", + "Air-gapped update with (soon)", + "[/color]", + "[/size]", + "\n", + f"[size={size[1]}sp]", + "[color=#333333]", + p, + "[/color]", + "[/size]", + ] + ) + + # default assertions + self.assertEqual(screen.ids[f"{screen.id}_airgap_button"].text, text) + self.assertTrue( + hasattr(UnzipStableScreen, f"on_press_{screen.id}_airgap_button") + ) + self.assertTrue( + hasattr(UnzipStableScreen, f"on_release_{screen.id}_airgap_button") + ) + + # patch assertions + mock_get_destdir_assets.assert_any_call() + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", return_value="mock" + ) + @patch("src.app.screens.unzip_stable_screen.UnzipStableScreen.set_background") + def test_on_press_flash_button( + self, mock_set_background, mock_get_destdir_assets, mock_get_locale + ): + screen = UnzipStableScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + size = [0, 0] + + if sys.platform in ("linux", "win32"): + size = [window.size[0] // 24, window.size[0] // 48] + + if sys.platform == "darwin": + size = [window.size[0] // 48, window.size[0] // 128] + + # DO tests + screen.update(name="VerifyStableZipScreen", key="device", value="mock") + screen.update(name="VerifyStableZipScreen", key="version", value="v0.0.1") + screen.update(name="VerifyStableZipScreen", key="flash-button") + button = screen.ids[f"{screen.id}_flash_button"] + action = getattr(screen, f"on_press_{screen.id}_flash_button") + action(button) + + p = os.path.join("mock", "krux-v0.0.1", "maixpy_mock", "kboot.kfpkg") + text = "".join( + [ + f"[size={size[0]}sp]", + "Extracting", + "[/size]", + "\n", + f"[size={size[1]}sp]", + "[color=#efcc00]", + p, + "[/color]", + "[/size]", + ] + ) + + # default assertions + self.assertEqual(button.text, text) + + # patch assertions + mock_get_destdir_assets.assert_called_once() + mock_get_locale.assert_called() + mock_set_background.assert_called_once_with( + wid=button.id, rgba=(0.25, 0.25, 0.25, 1) + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", return_value="mock" + ) + @patch("src.app.screens.unzip_stable_screen.UnzipStableScreen.set_background") + def test_on_press_airgap_button( + self, mock_set_background, mock_get_destdir_assets, mock_get_locale + ): + screen = UnzipStableScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + size = [0, 0] + + if sys.platform in ("linux", "win32"): + size = [window.size[0] // 24, window.size[0] // 48] + + if sys.platform == "darwin": + size = [window.size[0] // 48, window.size[0] // 128] + + # DO tests + screen.update(name="VerifyStableZipScreen", key="device", value="mock") + screen.update(name="VerifyStableZipScreen", key="version", value="v0.0.1") + screen.update(name="VerifyStableZipScreen", key="airgap-button") + button = screen.ids[f"{screen.id}_airgap_button"] + action = getattr(screen, f"on_press_{screen.id}_airgap_button") + action(button) + + p = os.path.join("mock", "krux-v0.0.1", "maixpy_mock", "firmware.bin") + text = "".join( + [ + f"[size={size[0]}sp]", + "[color=#333333]", + "Air-gapped update with (soon)", + "[/color]", + "[/size]", + "\n", + f"[size={size[1]}sp]", + "[color=#333333]", + p, + "[/color]", + "[/size]", + ] + ) + + # default assertions + self.assertEqual(button.text, text) + + # patch assertions + mock_get_destdir_assets.assert_called_once() + mock_get_locale.assert_called() + mock_set_background.assert_not_called() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", return_value="mock" + ) + @patch("src.app.screens.base_screen.BaseScreen.get_baudrate", return_value=1500000) + @patch("src.app.screens.unzip_stable_screen.UnzipStableScreen.set_background") + @patch("src.app.screens.unzip_stable_screen.KbootUnzip") + @patch("src.app.screens.unzip_stable_screen.UnzipStableScreen.manager") + @patch("src.app.screens.unzip_stable_screen.time.sleep") + def test_on_release_flash_button( + self, + mock_sleep, + mock_manager, + mock_kboot_unzip, + mock_set_background, + mock_get_baudrate, + mock_get_destdir_assets, + mock_get_locale, + ): + mock_kboot_unzip.load = MagicMock() + mock_manager.get_screen = MagicMock() + + screen = UnzipStableScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + size = [0, 0] + + if sys.platform in ("linux", "win32"): + size = [window.size[0] // 24, window.size[0] // 48] + + if sys.platform == "darwin": + size = [window.size[0] // 48, window.size[0] // 128] + + # DO tests + screen.update(name="VerifyStableZipScreen", key="device", value="mock") + screen.update(name="VerifyStableZipScreen", key="version", value="v0.0.1") + screen.update(name="VerifyStableZipScreen", key="flash-button") + button = screen.ids[f"{screen.id}_flash_button"] + action = getattr(screen, f"on_release_{screen.id}_flash_button") + action(button) + + p = os.path.join("mock", "krux-v0.0.1", "maixpy_mock", "kboot.kfpkg") + text = "".join( + [ + f"[size={size[0]}sp]", + "Extracted", + "[/size]", + "\n", + f"[size={size[1]}sp]", + "[color=#efcc00]", + p, + "[/color]", + "[/size]", + ] + ) + + # default assertions + self.assertEqual(button.text, text) + + # patch assertions + mock_get_baudrate.assert_called() + mock_get_destdir_assets.assert_called_once() + mock_get_locale.assert_called() + mock_get_destdir_assets.assert_called_once() + mock_kboot_unzip.assert_called_once_with( + filename=os.path.join("mock", "krux-v0.0.1.zip"), + device="mock", + output="mock", + ) + # mock_kboot_unzip.load.assert_called_once() + mock_set_background.assert_called_once_with(wid=button.id, rgba=(0, 0, 0, 1)) + mock_manager.get_screen.assert_called_once_with("FlashScreen") + mock_sleep.assert_called_once_with(2.1) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", return_value="mock" + ) + @patch("src.app.screens.unzip_stable_screen.UnzipStableScreen.set_background") + @patch("src.app.screens.unzip_stable_screen.FirmwareUnzip") + @patch("src.app.screens.unzip_stable_screen.UnzipStableScreen.manager") + @patch("src.app.screens.unzip_stable_screen.time.sleep") + def test_on_release_airgapped_button( + self, + mock_sleep, + mock_manager, + mock_firmware_unzip, + mock_set_background, + mock_get_destdir_assets, + mock_get_locale, + ): + mock_firmware_unzip.load = MagicMock() + mock_manager.get_screen = MagicMock() + + screen = UnzipStableScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + size = [0, 0] + + if sys.platform in ("linux", "win32"): + size = [window.size[0] // 24, window.size[0] // 48] + + if sys.platform == "darwin": + size = [window.size[0] // 48, window.size[0] // 128] + + # DO tests + screen.update(name="VerifyStableZipScreen", key="device", value="mock") + screen.update(name="VerifyStableZipScreen", key="version", value="v0.0.1") + screen.update(name="VerifyStableZipScreen", key="airgap-button") + button = screen.ids[f"{screen.id}_airgap_button"] + action = getattr(screen, f"on_release_{screen.id}_airgap_button") + action(button) + + p = os.path.join("mock", "krux-v0.0.1", "maixpy_mock", "firmware.bin") + text = "".join( + [ + f"[size={size[0]}sp]", + "[color=#333333]", + "Air-gapped update with (soon)", + "[/color]", + "[/size]", + "\n", + f"[size={size[1]}sp]", + "[color=#333333]", + p, + "[/color]", + "[/size]", + ] + ) + + # default assertions + self.assertEqual(button.text, text) + + # patch assertions + mock_get_destdir_assets.assert_called_once() + mock_get_locale.assert_called() + mock_firmware_unzip.assert_not_called() + mock_set_background.assert_not_called() + mock_manager.get_screen.assert_not_called() + mock_sleep.assert_not_called() diff --git a/e2e/test_017_verify_stable_zip_screen.py b/e2e/test_017_verify_stable_zip_screen.py new file mode 100644 index 00000000..d262308f --- /dev/null +++ b/e2e/test_017_verify_stable_zip_screen.py @@ -0,0 +1,362 @@ +import sys +from unittest.mock import patch, call, MagicMock +from kivy.base import EventLoop, EventLoopBase +from kivy.tests.common import GraphicUnitTest +from src.app.screens.verify_stable_zip_screen import ( + VerifyStableZipScreen, +) + + +class TestVerifyStableZipScreen(GraphicUnitTest): + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_init(self, mock_get_locale): + screen = VerifyStableZipScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + # default assertions + self.assertEqual(grid.id, f"{screen.id}_grid") + self.assertFalse(screen.success) + self.assertTrue( + hasattr(VerifyStableZipScreen, "on_ref_press_verify_stable_zip_screen") + ) + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.verify_stable_zip_screen.Sha256Verifyer") + @patch("src.app.screens.verify_stable_zip_screen.Sha256CheckVerifyer") + def test_verify_sha256(self, mock_check_verifyer, mock_verifyer, mock_get_locale): + screen = VerifyStableZipScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + screen.verify_sha256(assets_dir="mockdir", version="v0.0.1") + + # patch assertions + mock_get_locale.assert_called() + mock_verifyer.assert_called_once_with(filename="mockdir/krux-v0.0.1.zip") + mock_check_verifyer.assert_called_once_with( + filename="mockdir/krux-v0.0.1.zip.sha256.txt" + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.verify_stable_zip_screen.SigVerifyer") + @patch("src.app.screens.verify_stable_zip_screen.PemCheckVerifyer") + @patch("src.app.screens.verify_stable_zip_screen.SigCheckVerifyer") + def test_verify_signature( + self, + mock_sig_check_verifyer, + mock_pem_check_verifyer, + mock_sig_verifyer, + mock_get_locale, + ): + screen = VerifyStableZipScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + screen.verify_signature(assets_dir="mockdir", version="v0.0.1") + + # patch assertions + mock_get_locale.assert_called() + mock_sig_check_verifyer.assert_called_once_with( + filename="mockdir/krux-v0.0.1.zip.sig" + ) + mock_pem_check_verifyer.assert_called_once_with( + filename="mockdir/selfcustody.pem" + ) + mock_sig_verifyer.assert_called_once_with( + filename="mockdir/krux-v0.0.1.zip", + regexp=r"^.*\.zip$", + signature=mock_sig_check_verifyer().data, + pubkey=mock_pem_check_verifyer().data, + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_on_pre_enter(self, mock_get_locale): + screen = VerifyStableZipScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + fontsize_mm = 0 + + if sys.platform in ("linux", "win32"): + fontsize_mm = window.size[0] // 24 + + if sys.platform == "darwin": + fontsize_mm = window.size[0] // 48 + + screen.on_pre_enter() + + text = "".join( + [ + f"[size={fontsize_mm}sp]", + "[color=#efcc00]", + "Verifying integrity and authenticity", + "[/color]", + "[/size]", + ] + ) + self.assertEqual(len(screen.ids["verify_stable_zip_screen_grid"].children), 1) + self.assertEqual(screen.ids["verify_stable_zip_screen_label"].text, text) + + # patch assertions + mock_get_locale.assert_called() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.verify_stable_zip_screen.VerifyStableZipScreen.manager") + @patch( + "src.app.screens.verify_stable_zip_screen.VerifyStableZipScreen.get_destdir_assets" + ) + @patch( + "src.app.screens.verify_stable_zip_screen.VerifyStableZipScreen.verify_sha256" + ) + @patch( + "src.app.screens.verify_stable_zip_screen.VerifyStableZipScreen.verify_signature" + ) + def test_on_enter( + self, + mock_verify_signature, + mock_verify_sha256, + mock_get_destdir_assets, + mock_manager, + mock_get_locale, + ): + sha_text = "\n".join( + [ + "[size=20sp][color=#efcc00]Integrity verification:[/color][/size]", + "", + "[size=16sp][b]mockdir/krux-v0.0.1.zip[/b][/size]", + "[size=14sp][color=#00FF00]mocksha2560123456789abcdef[/color][/size]", + "", + "[size=16sp][b]mockdir/krux-v0.0.1.zip.sha256.txt[/b][/size]", + "[size=14sp][color=#00FF00]mocksha2560123456789abcdef[/color][/size]", + "[size=14sp]Result: SUCCESS[/b][/size]", + "", + "", + ] + ) + + sig_text = "\n".join( + [ + "[size=20sp][color=#efcc00]Authenticity verification:[/color][/size]", + "", + "[size=16sp]Result: [b]GOOD SIGNATURE[/b][/size]", + "", + "[size=16sp]If you have openssl installed on your system[/size]", + "[size=16sp]you can check manually with the following command:[/size]", + "", + "[color=#00ff00][size=14sp]openssl sha256< mockdir/krux-v0.0.1.zip -binary | \\", + "openssl pkeyutl -verify -pubin -inkey mockdir/selfcustody.pem \\", + "-sigfile mockdir/krux-v0.0.0.1.zip.sig[/size][/color]", + ] + ) + mock_manager.get_screen = MagicMock() + mock_get_destdir_assets.return_value = "mockdir" + mock_verify_sha256.return_value = sha_text + mock_verify_signature.return_value = sig_text + + screen = VerifyStableZipScreen() + screen.version = "v0.0.1" + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + screen.on_pre_enter() + screen.on_enter() + + full_text = sha_text + sig_text + self.assertEqual(screen.ids["verify_stable_zip_screen_label"].text, full_text) + + # patch assertions + mock_get_locale.assert_called() + mock_get_destdir_assets.assert_called_once() + mock_manager.get_screen.assert_called_once_with("MainScreen") + mock_verify_sha256.assert_called_once_with( + assets_dir="mockdir", version=mock_manager.get_screen().version + ) + mock_verify_signature.assert_called_once_with( + assets_dir="mockdir", version=mock_manager.get_screen().version + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update_invalid_name(self, mock_redirect_error, mock_get_locale): + screen = VerifyStableZipScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name="MockScreen") + + # patch assertions + mock_get_locale.assert_called() + mock_redirect_error.assert_called_once_with("Invalid screen name: MockScreen") + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update_key(self, mock_redirect_error, mock_get_locale): + screen = VerifyStableZipScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + screen.update(name=screen.name, key="mock") + + # patch assertions + mock_get_locale.assert_called() + mock_redirect_error.assert_called_once_with('Invalid key: "mock"') + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_locale(self, mock_get_locale): + screen = VerifyStableZipScreen() + old_loc = screen.locale + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # do tests + screen.update(name="ConfigKruxInstaller", key="locale", value="pt_BR.UTF-8") + + # default assertions + self.assertEqual(screen.locale, "pt_BR.UTF-8") + self.assertFalse(screen.locale == old_loc) + + # patch assertions + mock_get_locale.assert_called() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.verify_stable_zip_screen.VerifyStableZipScreen.manager") + @patch("src.app.screens.verify_stable_zip_screen.partial") + @patch("src.app.screens.verify_stable_zip_screen.Clock.schedule_once") + @patch("src.app.screens.verify_stable_zip_screen.VerifyStableZipScreen.set_screen") + def test_on_press_proceed( + self, + mock_set_screen, + mock_schedule_once, + mock_partial, + mock_manager, + mock_get_locale, + ): + screen = VerifyStableZipScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # DO tests + screen.on_pre_enter() + action = getattr(VerifyStableZipScreen, f"on_ref_press_{screen.id}") + action("Mock", "UnzipStableScreen") + + # patch assertions + mock_get_locale.assert_called() + mock_manager.get_screen.assert_has_calls( + [call("MainScreen"), call("UnzipStableScreen")] + ) + mock_partial.assert_has_calls( + [ + call( + mock_manager.get_screen().update, + name="VerifyStableZipScreen", + key="version", + value=mock_manager.get_screen().version, + ), + call( + mock_manager.get_screen().update, + name="VerifyStableZipScreen", + key="device", + value=mock_manager.get_screen().device, + ), + call( + mock_manager.get_screen().update, + name="VerifyStableZipScreen", + key="clear", + ), + call( + mock_manager.get_screen().update, + name="VerifyStableZipScreen", + key="flash-button", + ), + call( + mock_manager.get_screen().update, + name="VerifyStableZipScreen", + key="airgap-button", + ), + ] + ) + + mock_schedule_once.assert_has_calls([call(mock_partial(), 0)]) + mock_set_screen.assert_called_once_with( + name="UnzipStableScreen", direction="left" + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.verify_stable_zip_screen.VerifyStableZipScreen.set_screen") + def test_on_press_back(self, mock_set_screen, mock_get_locale): + screen = VerifyStableZipScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # DO tests + screen.on_pre_enter() + action = getattr(VerifyStableZipScreen, f"on_ref_press_{screen.id}") + action("Mock", "MainScreen") + + # patch assertions + mock_get_locale.assert_called() + mock_set_screen.assert_called_once_with(name="MainScreen", direction="right") diff --git a/e2e/test_018_base_krux_installer.py b/e2e/test_018_base_krux_installer.py new file mode 100644 index 00000000..d8db5084 --- /dev/null +++ b/e2e/test_018_base_krux_installer.py @@ -0,0 +1,34 @@ +import os +from unittest.mock import patch +from kivy.base import EventLoop, EventLoopBase +from kivy.tests.common import GraphicUnitTest +from src.app.base_krux_installer import BaseKruxInstaller + + +class TestBaseKruxInstaller(GraphicUnitTest): + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.base_krux_installer.Logger.setLevel") + def test_init(self, mock_set_level): + app = BaseKruxInstaller() + + self.assertEqual(len(app.screens), 0) + self.assertFalse(app.screen_manager is None) + + mock_set_level.assert_called_once_with(20) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch.dict(os.environ, {"LOGLEVEL": "debug"}, clear=True) + @patch("src.app.base_krux_installer.Logger.setLevel") + def test_init_debug(self, mock_set_level): + + app = BaseKruxInstaller() + + self.assertEqual(len(app.screens), 0) + self.assertFalse(app.screen_manager is None) + + mock_set_level.assert_called_once_with(10) diff --git a/e2e/test_019_config_krux_installer.py b/e2e/test_019_config_krux_installer.py new file mode 100644 index 00000000..544e5c9c --- /dev/null +++ b/e2e/test_019_config_krux_installer.py @@ -0,0 +1,796 @@ +import os +import sys +import json +from unittest.mock import patch, MagicMock, call, mock_open +from kivy.base import EventLoop +from kivy.tests.common import GraphicUnitTest +from src.app.config_krux_installer import ConfigKruxInstaller + + +class TestConfigKruxInstaller(GraphicUnitTest): + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.dict(os.environ, {"LANG": "en-US.UTF-8"}, clear=True) + @patch("sys.platform", "linux") + def test_get_system_lang_linux(self): + lang = ConfigKruxInstaller.get_system_lang() + self.assertEqual(lang, "en-US.UTF-8") + + @patch.dict(os.environ, {"LANG": "en-US.UTF-8"}, clear=True) + @patch("sys.platform", "darwin") + def test_get_system_lang_darwin(self): + lang = ConfigKruxInstaller.get_system_lang() + self.assertEqual(lang, "en-US.UTF-8") + + @patch.dict(os.environ, {"LANG": "en-US.UTF-8"}, clear=True) + @patch("sys.platform", "win32") + @patch("src.app.config_krux_installer.ctypes") + @patch("src.app.config_krux_installer.locale") + def test_get_system_lang_win32(self, mock_locale, mock_ctypes): + mock_ctypes = MagicMock() + mock_ctypes.windll = MagicMock + mock_ctypes.windll.kernel32 = MagicMock() + mock_ctypes.windll.kernel32.GetUserDefaultUILanguage = MagicMock( + return_value="en" + ) + + mock_locale = MagicMock() + mock_locale.windows_locale = {"en": "en-US.UTF-8"} + + ConfigKruxInstaller.get_system_lang() + + mock_ctypes.windll.kernel32.GetUserDefaultUILanguage.assert_called_once() + + @patch.dict(os.environ, {"LANG": "en-US.UTF-8"}, clear=True) + @patch("sys.platform", "mockos") + def test_fail_get_system_lang(self): + with self.assertRaises(OSError) as exc_info: + ConfigKruxInstaller.get_system_lang() + + self.assertEqual(str(exc_info.exception), "OS 'mockos' not recognized") + + @patch("sys.platform", "linux") + @patch("src.app.config_krux_installer.os.path.expanduser", return_value="mockdir") + def test_get_app_dir_config_linux(self, mock_expanduser): + app = ConfigKruxInstaller() + _dir = app.get_app_dir(name="config") + + self.assertEqual(_dir, os.path.join("mockdir", ".config", "krux-installer")) + + # patch assertions + mock_expanduser.assert_called_once_with("~") + + @patch.dict(os.environ, {"LOCALAPPDATA": "mockdir"}, clear=True) + @patch("sys.platform", "win32") + def test_get_app_dir_config_win32(self): + app = ConfigKruxInstaller() + _dir = app.get_app_dir(name="config") + + self.assertEqual(_dir, os.path.join("mockdir", "krux-installer", "config")) + + @patch.dict(os.environ, {"LOCALAPPDATA": ""}, clear=True) + @patch("sys.platform", "win32") + def test_fail_get_app_dir_config_win32_empty(self): + app = ConfigKruxInstaller() + with self.assertRaises(EnvironmentError) as exc_info: + app.get_app_dir(name="config") + + self.assertEqual(str(exc_info.exception), "LOCALAPPDATA is empty") + + @patch.dict(os.environ, {"MOCKAPPDATA": "mockdir"}, clear=True) + @patch("sys.platform", "win32") + def test_fail_get_app_dir_config_win32_not_found(self): + app = ConfigKruxInstaller() + with self.assertRaises(EnvironmentError) as exc_info: + app.get_app_dir(name="config") + + self.assertEqual( + str(exc_info.exception), "LOCALAPPDATA environment variable not found" + ) + + @patch("sys.platform", "darwin") + @patch("src.app.config_krux_installer.os.path.expanduser", return_value="mockdir") + def test_get_app_dir_config_darwin(self, mock_expanduser): + app = ConfigKruxInstaller() + _dir = app.get_app_dir(name="config") + + self.assertEqual(_dir, os.path.join("mockdir", ".config", "krux-installer")) + + # patch assertions + mock_expanduser.assert_called_once_with("~") + + @patch("sys.platform", "mockos") + def test_fail_get_app_dir_config_wrong_name(self): + app = ConfigKruxInstaller() + + with self.assertRaises(ValueError) as exc_info: + app.get_app_dir(name="mock") + + self.assertEqual(str(exc_info.exception), "Invalid name: 'mock'") + + @patch("sys.platform", "mockos") + def test_fail_get_app_dir_config_wrong_os(self): + app = ConfigKruxInstaller() + + with self.assertRaises(OSError) as exc_info: + app.get_app_dir(name="config") + + self.assertEqual(str(exc_info.exception), "Not supported: mockos") + + @patch("sys.platform", "linux") + @patch("src.app.config_krux_installer.os.path.expanduser", return_value="mockdir") + @patch("src.app.config_krux_installer.os.path.exists", side_effect=[False]) + @patch("src.app.config_krux_installer.os.makedirs") + def test_create_app_dir_linux(self, mock_makedirs, mock_exists, mock_expanduser): + app = ConfigKruxInstaller() + _dir = app.create_app_dir(name="config") + dir_test = os.path.join("mockdir", ".config", "krux-installer") + + self.assertEqual(_dir, dir_test) + + # patch assertions + mock_expanduser.assert_called_once_with("~") + mock_exists.assert_called_once_with(dir_test) + mock_makedirs.assert_called_once_with(dir_test) + + @patch("sys.platform", "linux") + @patch("src.app.config_krux_installer.os.path.expanduser", return_value="mockdir") + @patch("src.app.config_krux_installer.os.path.exists", side_effect=[True]) + @patch("src.app.config_krux_installer.os.makedirs") + def test_skip_create_app_dir_linux( + self, mock_makedirs, mock_exists, mock_expanduser + ): + app = ConfigKruxInstaller() + _dir = app.create_app_dir(name="config") + dir_test = os.path.join("mockdir", ".config", "krux-installer") + + self.assertEqual(_dir, dir_test) + + # patch assertions + mock_expanduser.assert_called_once_with("~") + mock_exists.assert_called_once_with(dir_test) + assert len(mock_makedirs.mock_calls) == 0 + + @patch.dict(os.environ, {"LOCALAPPDATA": "mockdir"}, clear=True) + @patch("sys.platform", "win32") + @patch("src.app.config_krux_installer.os.path.exists", side_effect=[False]) + @patch("src.app.config_krux_installer.os.makedirs") + def test_create_app_dir_win32(self, mock_makedirs, mock_exists): + app = ConfigKruxInstaller() + _dir = app.create_app_dir(name="config") + dir_test = os.path.join("mockdir", "krux-installer", "config") + + self.assertEqual(_dir, dir_test) + + # patch assertions + mock_exists.assert_called_once_with(dir_test) + mock_makedirs.assert_called_once_with(dir_test) + + @patch.dict(os.environ, {"LOCALAPPDATA": "mockdir"}, clear=True) + @patch("sys.platform", "win32") + @patch("src.app.config_krux_installer.os.path.exists", side_effect=[True]) + @patch("src.app.config_krux_installer.os.makedirs") + def test_skip_create_app_dir_win32(self, mock_makedirs, mock_exists): + app = ConfigKruxInstaller() + _dir = app.create_app_dir(name="config") + dir_test = os.path.join("mockdir", "krux-installer", "config") + + self.assertEqual(_dir, dir_test) + + # patch assertions + mock_exists.assert_called_once_with(dir_test) + assert len(mock_makedirs.mock_calls) == 0 + + @patch("sys.platform", "darwin") + @patch("src.app.config_krux_installer.os.path.expanduser", return_value="mockdir") + @patch("src.app.config_krux_installer.os.path.exists", side_effect=[False]) + @patch("src.app.config_krux_installer.os.makedirs") + def test_create_app_dir_darwin(self, mock_makedirs, mock_exists, mock_expanduser): + app = ConfigKruxInstaller() + _dir = app.create_app_dir(name="config") + dir_test = os.path.join("mockdir", ".config", "krux-installer") + + self.assertEqual(_dir, dir_test) + + # patch assertions + mock_expanduser.assert_called_once_with("~") + mock_exists.assert_called_once_with(dir_test) + mock_makedirs.assert_called_once_with(dir_test) + + @patch("sys.platform", "darwin") + @patch("src.app.config_krux_installer.os.path.expanduser", return_value="mockdir") + @patch("src.app.config_krux_installer.os.path.exists", side_effect=[True]) + @patch("src.app.config_krux_installer.os.makedirs") + def test_skip_create_app_dir_darwin( + self, mock_makedirs, mock_exists, mock_expanduser + ): + app = ConfigKruxInstaller() + _dir = app.create_app_dir(name="config") + dir_test = os.path.join("mockdir", ".config", "krux-installer") + + self.assertEqual(_dir, dir_test) + + # patch assertions + mock_expanduser.assert_called_once_with("~") + mock_exists.assert_called_once_with(dir_test) + assert len(mock_makedirs.mock_calls) == 0 + + @patch("sys.platform", "linux") + @patch("src.app.config_krux_installer.os.path.expanduser", return_value="mockdir") + @patch("src.app.config_krux_installer.os.path.exists", side_effect=[False, False]) + @patch("builtins.open", new_callable=mock_open) + def test_create_app_file_linux(self, open_mock, mock_exists, mock_expanduser): + app = ConfigKruxInstaller() + file = app.create_app_file(context="config", name="config.ini") + file_test = os.path.join("mockdir", ".config", "krux-installer", "config.ini") + + self.assertEqual(file, file_test) + + # patch assertions + mock_expanduser.assert_called_once_with("~") + mock_exists.assert_called_once_with(file_test) + open_mock.assert_has_calls( + [ + call(file_test, "w", encoding="utf8"), + call().write("# Generated config. Do not edit this manually!\n"), + ], + any_order=True, + ) + + @patch("sys.platform", "linux") + @patch("src.app.config_krux_installer.os.path.expanduser", return_value="mockdir") + @patch("src.app.config_krux_installer.os.path.exists", side_effect=[True, True]) + @patch("builtins.open", new_callable=mock_open) + def test_skip_create_app_file_linux(self, open_mock, mock_exists, mock_expanduser): + app = ConfigKruxInstaller() + file = app.create_app_file(context="config", name="config.ini") + file_test = os.path.join("mockdir", ".config", "krux-installer", "config.ini") + + self.assertEqual(file, file_test) + + # patch assertions + mock_expanduser.assert_called_once_with("~") + mock_exists.assert_called_once_with(file_test) + assert len(open_mock.mock_calls) == 0 + + @patch.dict(os.environ, {"LOCALAPPDATA": "mockdir"}, clear=True) + @patch("sys.platform", "win32") + @patch("src.app.config_krux_installer.os.path.exists", side_effect=[False, False]) + @patch("builtins.open", new_callable=mock_open) + def test_create_app_file_win32(self, open_mock, mock_exists): + app = ConfigKruxInstaller() + file = app.create_app_file(context="config", name="config.ini") + file_test = os.path.join("mockdir", "krux-installer", "config", "config.ini") + + self.assertEqual(file, file_test) + + # patch assertions + mock_exists.assert_called_once_with(file_test) + open_mock.assert_has_calls( + [ + call(file_test, "w", encoding="utf8"), + call().write("# Generated config. Do not edit this manually!\n"), + ], + any_order=True, + ) + + @patch.dict(os.environ, {"LOCALAPPDATA": "mockdir"}, clear=True) + @patch("sys.platform", "win32") + @patch("src.app.config_krux_installer.os.path.exists", side_effect=[True, True]) + @patch("builtins.open", new_callable=mock_open) + def test_skip_create_app_file_win32(self, open_mock, mock_exists): + app = ConfigKruxInstaller() + file = app.create_app_file(context="config", name="config.ini") + file_test = os.path.join("mockdir", "krux-installer", "config", "config.ini") + + self.assertEqual(file, file_test) + + # patch assertions + mock_exists.assert_called_once_with(file_test) + assert len(open_mock.mock_calls) == 0 + + @patch("sys.platform", "darwin") + @patch("src.app.config_krux_installer.os.path.expanduser", return_value="mockdir") + @patch("src.app.config_krux_installer.os.path.exists", side_effect=[False, False]) + @patch("builtins.open", new_callable=mock_open) + def test_create_app_file_darwin(self, open_mock, mock_exists, mock_expanduser): + app = ConfigKruxInstaller() + file = app.create_app_file(context="config", name="config.ini") + file_test = os.path.join( + "mockdir", + ".config", + "krux-installer", + "config.ini", + ) + + self.assertEqual(file, file_test) + + # patch assertions + mock_expanduser.assert_called_once_with("~") + mock_exists.assert_called_once_with(file_test) + open_mock.assert_has_calls( + [ + call(file_test, "w", encoding="utf8"), + call().write("# Generated config. Do not edit this manually!\n"), + ], + any_order=True, + ) + + @patch("sys.platform", "darwin") + @patch("src.app.config_krux_installer.os.path.expanduser", return_value="mockdir") + @patch("src.app.config_krux_installer.os.path.exists", side_effect=[True, True]) + @patch("builtins.open", new_callable=mock_open) + def test_skip_create_app_file_darwin(self, open_mock, mock_exists, mock_expanduser): + app = ConfigKruxInstaller() + file = app.create_app_file(context="config", name="config.ini") + file_test = os.path.join( + "mockdir", + ".config", + "krux-installer", + "config.ini", + ) + + self.assertEqual(file, file_test) + + # patch assertions + mock_expanduser.assert_called_once_with("~") + mock_exists.assert_called_once_with(file_test) + assert len(open_mock.mock_calls) == 0 + + @patch("src.app.config_krux_installer.ConfigKruxInstaller.create_app_dir") + @patch("src.app.config_krux_installer.ConfigKruxInstaller.create_app_file") + @patch("src.app.config_krux_installer.BaseKruxInstaller.get_application_config") + def test_get_application_config( + self, mock_get_application_config, mock_create_app_file, mock_create_app_dir + ): + mock_create_app_file.return_value = os.path.join("mockfile") + app = ConfigKruxInstaller() + app.get_application_config() + + # patch assertions + mock_create_app_dir.assert_called_once_with(name="config") + mock_create_app_file.assert_called_once_with( + context="config", name="config.ini" + ) + mock_get_application_config.assert_called_once_with("mockfile") + + @patch("src.app.config_krux_installer.ConfigKruxInstaller.create_app_dir") + @patch("src.app.config_krux_installer.ConfigKruxInstaller.get_system_lang") + def test_build_config(self, mock_get_system_lang, mock_create_app_dir): + mock_create_app_dir.return_value = "mockdir" + config = MagicMock() + config.setdefaults = MagicMock() + mock_get_system_lang.return_value = "en_US.UTF-8" + + app = ConfigKruxInstaller() + app.build_config(config) + + # patch assertions + mock_create_app_dir.assert_called_once_with(name="local") + + if sys.platform in ("linux", "darwin"): + lang = "en_US.UTF-8" + else: + lang = "en_US" + + config.setdefaults.assert_has_calls( + [ + call("destdir", {"assets": "mockdir"}), + call("flash", {"baudrate": 1500000}), + call("locale", {"lang": lang}), + ] + ) + + def test_build_settings(self): + settings = MagicMock() + settings.add_json_panel = MagicMock() + app = ConfigKruxInstaller() + app.build_settings(settings) + + json_data = [ + { + "type": "path", + "title": "Assets's destination path", + "desc": "Destination path of downloaded assets", + "section": "destdir", + "key": "assets", + }, + { + "type": "numeric", + "title": "Flash baudrate", + "desc": "Applied baudrate during the flash process", + "section": "flash", + "key": "baudrate", + }, + { + "type": "options", + "title": "Locale", + "desc": "Application locale", + "section": "locale", + "key": "lang", + "options": [ + f"af_ZA{".UTF-8" if sys.platform in ("linux", "darwin") else ""}", + f"en_US{".UTF-8" if sys.platform in ("linux", "darwin") else ""}", + f"es_ES{".UTF-8" if sys.platform in ("linux", "darwin") else ""}", + f"fr_FR{".UTF-8" if sys.platform in ("linux", "darwin") else ""}", + f"it_IT{".UTF-8" if sys.platform in ("linux", "darwin") else ""}", + f"ko_KR{".UTF-8" if sys.platform in ("linux", "darwin") else ""}", + f"nl_NL{".UTF-8" if sys.platform in ("linux", "darwin") else ""}", + f"pt_BR{".UTF-8" if sys.platform in ("linux", "darwin") else ""}", + f"ru_RU{".UTF-8" if sys.platform in ("linux", "darwin") else ""}", + f"zh_CN{".UTF-8" if sys.platform in ("linux", "darwin") else ""}", + ], + }, + ] + + # patch assertions + settings.add_json_panel.assert_has_calls( + [call("Settings", None, data=json.dumps(json_data))], any_order=True + ) + + @patch("src.app.config_krux_installer.partial") + def test_skip_on_config_change_linux(self, mock_partial): + app = ConfigKruxInstaller() + + # Do tests + app.on_config_change(None, "test", key="mock", value="skip") + assert len(mock_partial.mock_calls) == 0 + + @patch("sys.platform", "linux") + @patch("src.app.config_krux_installer.partial") + def test_on_config_change_linux(self, mock_partial): + + app = ConfigKruxInstaller() + + app.screen_manager = MagicMock() + app.screen_manager.get_screen = MagicMock() + app.screens = [ + MagicMock(name="MainScreen"), + MagicMock(name="SelectVersionScreen"), + MagicMock(name="SelectOldVersionScreen"), + MagicMock(name="WarningAlreadyDownloadedScreen"), + MagicMock(name="WarningBetaScreen"), + MagicMock(name="VerifyStableZipScreen"), + MagicMock(name="UnzipStableScreen"), + MagicMock(name="CheckPermissionsScreen"), + ] + + # Do tests + app.on_config_change(None, "locale", key="lang", value="mock") + + # patch assertions + calls_get_screen = [ + call("MainScreen"), + call("SelectVersionScreen"), + call("SelectOldVersionScreen"), + call("WarningAlreadyDownloadedScreen"), + call("WarningBetaScreen"), + call("VerifyStableZipScreen"), + call("UnzipStableScreen"), + call("CheckPermissionsScreen"), + ] + + calls_partial = [ + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="locale", + value="mock", + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="version", + value=app.screen_manager.get_screen().version, + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="device", + value=app.screen_manager.get_screen().device, + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="flash", + value=None, + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="wipe", + value=None, + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="settings", + value=None, + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="about", + value=None, + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="locale", + value="mock", + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="locale", + value="mock", + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="locale", + value="mock", + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="locale", + value="mock", + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="locale", + value="mock", + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="locale", + value="mock", + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="locale", + value="mock", + ), + ] + + app.screen_manager.get_screen.assert_has_calls(calls_get_screen, any_order=True) + mock_partial.assert_has_calls(calls_partial) + + @patch("sys.platform", "win32") + @patch("src.app.config_krux_installer.partial") + def test_on_config_change_win32(self, mock_partial): + + app = ConfigKruxInstaller() + + app.screen_manager = MagicMock() + app.screen_manager.get_screen = MagicMock() + app.screens = [ + MagicMock(name="MainScreen"), + MagicMock(name="SelectVersionScreen"), + MagicMock(name="SelectOldVersionScreen"), + MagicMock(name="WarningAlreadyDownloadedScreen"), + MagicMock(name="WarningBetaScreen"), + MagicMock(name="VerifyStableZipScreen"), + MagicMock(name="UnzipStableScreen"), + ] + + # Do tests + app.on_config_change(None, "locale", key="lang", value="mock") + + # patch assertions + calls_get_screen = [ + call("MainScreen"), + call("SelectVersionScreen"), + call("SelectOldVersionScreen"), + call("WarningAlreadyDownloadedScreen"), + call("WarningBetaScreen"), + call("VerifyStableZipScreen"), + call("UnzipStableScreen"), + ] + + calls_partial = [ + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="locale", + value="mock.UTF-8", + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="version", + value=app.screen_manager.get_screen().version, + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="device", + value=app.screen_manager.get_screen().device, + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="flash", + value=None, + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="wipe", + value=None, + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="settings", + value=None, + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="about", + value=None, + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="locale", + value="mock.UTF-8", + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="locale", + value="mock.UTF-8", + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="locale", + value="mock.UTF-8", + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="locale", + value="mock.UTF-8", + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="locale", + value="mock.UTF-8", + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="locale", + value="mock.UTF-8", + ), + ] + + app.screen_manager.get_screen.assert_has_calls(calls_get_screen, any_order=True) + mock_partial.assert_has_calls(calls_partial) + + @patch("sys.platform", "darwin") + @patch("src.app.config_krux_installer.partial") + def test_on_config_change_darwin(self, mock_partial): + + app = ConfigKruxInstaller() + + app.screen_manager = MagicMock() + app.screen_manager.get_screen = MagicMock() + app.screens = [ + MagicMock(name="MainScreen"), + MagicMock(name="SelectVersionScreen"), + MagicMock(name="SelectOldVersionScreen"), + MagicMock(name="WarningAlreadyDownloadedScreen"), + MagicMock(name="WarningBetaScreen"), + MagicMock(name="VerifyStableZipScreen"), + MagicMock(name="UnzipStableScreen"), + ] + + # Do tests + app.on_config_change(None, "locale", key="lang", value="mock") + + # patch assertions + calls_get_screen = [ + call("MainScreen"), + call("SelectVersionScreen"), + call("SelectOldVersionScreen"), + call("WarningAlreadyDownloadedScreen"), + call("WarningBetaScreen"), + call("VerifyStableZipScreen"), + call("UnzipStableScreen"), + ] + + calls_partial = [ + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="locale", + value="mock", + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="version", + value=app.screen_manager.get_screen().version, + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="device", + value=app.screen_manager.get_screen().device, + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="flash", + value=None, + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="wipe", + value=None, + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="settings", + value=None, + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="about", + value=None, + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="locale", + value="mock", + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="locale", + value="mock", + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="locale", + value="mock", + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="locale", + value="mock", + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="locale", + value="mock", + ), + call( + app.screen_manager.get_screen().update, + name="ConfigKruxInstaller", + key="locale", + value="mock", + ), + ] + + app.screen_manager.get_screen.assert_has_calls(calls_get_screen, any_order=True) + mock_partial.assert_has_calls(calls_partial) diff --git a/e2e/test_020_app_init.py b/e2e/test_020_app_init.py new file mode 100644 index 00000000..4152aeed --- /dev/null +++ b/e2e/test_020_app_init.py @@ -0,0 +1,221 @@ +import os +from unittest.mock import patch, MagicMock, call +from kivy.base import EventLoop, EventLoopBase +from kivy.tests.common import GraphicUnitTest +from kivy.uix.screenmanager import ScreenManager +from kivy.core.text import LabelBase, DEFAULT_FONT +from src.app import KruxInstallerApp + + +class TestConfigKruxInstaller(GraphicUnitTest): + + @classmethod + def setUpClass(cls): + cwd_path = os.path.dirname(__file__) + rel_assets_path = os.path.join(cwd_path, "..", "assets") + assets_path = os.path.abspath(rel_assets_path) + noto_sans_path = os.path.join(assets_path, "NotoSansCJK_Cy_SC_KR_Krux.ttf") + LabelBase.register(DEFAULT_FONT, noto_sans_path) + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch.dict(os.environ, {"LANG": "en_US.UTF-8"}, clear=True) + def test_init(self): + app = KruxInstallerApp() + self.assertEqual(len(app.screens), 0) + self.assertIsInstance(app.screen_manager, ScreenManager) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + def test_setup_screen_manager(self): + mock_screen = MagicMock(name="MockScreen") + + app = KruxInstallerApp() + app.screen_manager = MagicMock() + app.screen_manager.add_widget = MagicMock() + app.screens = [mock_screen] + + app.setup_screen_manager() + + # patch assertions + app.screen_manager.add_widget.assert_has_calls( + [ + call(mock_screen), + ] + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + def test_fail_setup_screen_manager(self): + app = KruxInstallerApp() + with self.assertRaises(RuntimeError) as exc_info: + app.setup_screen_manager() + + self.assertEqual( + str(exc_info.exception), "Cannot setup screen_manager: screen list is empty" + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("sys.platform", "linux") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", return_value="mock" + ) + def test_setup_screens(self, mock_get_destdir_assets, mock_get_locale): + app = KruxInstallerApp() + app.setup_screens() + + allowed_screens = ( + "GreetingsScreen", + "CheckPermissionsScreen", + "CheckInternetConnectionScreen", + "MainScreen", + "SelectDeviceScreen", + "SelectVersionScreen", + "SelectOldVersionScreen", + "WarningBetaScreen", + "AboutScreen", + "DownloadStableZipScreen", + "DownloadStableZipSha256Screen", + "DownloadStableZipSigScreen", + "DownloadSelfcustodyPemScreen", + "VerifyStableZipScreen", + "UnzipStableScreen", + "DownloadBetaScreen", + "WarningAlreadyDownloadedScreen", + "WarningWipeScreen", + "FlashScreen", + "WipeScreen", + "ErrorScreen", + ) + for screen in app.screens: + self.assertIn(screen.name, allowed_screens) + + mock_get_destdir_assets.assert_called_once() + mock_get_locale.assert_called() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("sys.platform", "win32") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", return_value="mock" + ) + def test_setup_screens_win32(self, mock_get_destdir_assets, mock_get_locale): + app = KruxInstallerApp() + app.setup_screens() + + allowed_screens = ( + "GreetingsScreen", + "CheckInternetConnectionScreen", + "MainScreen", + "SelectDeviceScreen", + "SelectVersionScreen", + "SelectOldVersionScreen", + "WarningBetaScreen", + "AboutScreen", + "DownloadStableZipScreen", + "DownloadStableZipSha256Screen", + "DownloadStableZipSigScreen", + "DownloadSelfcustodyPemScreen", + "VerifyStableZipScreen", + "UnzipStableScreen", + "DownloadBetaScreen", + "WarningAlreadyDownloadedScreen", + "WarningWipeScreen", + "FlashScreen", + "WipeScreen", + "ErrorScreen", + ) + for screen in app.screens: + self.assertIn(screen.name, allowed_screens) + + mock_get_destdir_assets.assert_called_once() + mock_get_locale.assert_called() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("sys.platform", "darwin") + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch( + "src.app.screens.base_screen.BaseScreen.get_destdir_assets", return_value="mock" + ) + def test_setup_screens_darwin(self, mock_get_destdir_assets, mock_get_locale): + app = KruxInstallerApp() + app.setup_screens() + + allowed_screens = ( + "GreetingsScreen", + "CheckInternetConnectionScreen", + "MainScreen", + "SelectDeviceScreen", + "SelectVersionScreen", + "SelectOldVersionScreen", + "WarningBetaScreen", + "AboutScreen", + "DownloadStableZipScreen", + "DownloadStableZipSha256Screen", + "DownloadStableZipSigScreen", + "DownloadSelfcustodyPemScreen", + "VerifyStableZipScreen", + "UnzipStableScreen", + "DownloadBetaScreen", + "WarningAlreadyDownloadedScreen", + "WarningWipeScreen", + "FlashScreen", + "WipeScreen", + "ErrorScreen", + ) + for screen in app.screens: + self.assertIn(screen.name, allowed_screens) + + mock_get_destdir_assets.assert_called_once() + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch.dict(os.environ, {"LANG": "en_US.UTF-8"}, clear=True) + @patch("src.app.KruxInstallerApp.screen_manager") + @patch("src.app.partial") + @patch("src.app.Clock.schedule_once") + def test_on_greetings(self, mock_schedule_once, mock_partial, mock_screen_manager): + mock_screen_manager.get_screen = MagicMock() + app = KruxInstallerApp() + app.on_greetings() + + mock_partial.assert_called_once_with( + mock_screen_manager.get_screen().update, + name="KruxInstallerApp", + key="check_permissions", + ) + mock_schedule_once.assert_called_once_with(mock_partial(), 0) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.KruxInstallerApp.on_greetings") + def test_on_start(self, mock_on_greetings): + app = KruxInstallerApp() + app.on_start() + mock_on_greetings.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.KruxInstallerApp.setup_screens") + @patch("src.app.KruxInstallerApp.setup_screen_manager") + # pylint: disable=unused-argument + def test_build( + self, + mock_setup_screen_manager, + mock_setup_screens, + mock_get_locale, + ): + app = KruxInstallerApp() + app.build() + + mock_setup_screens.assert_called_once() + mock_setup_screen_manager.assert_called_once() diff --git a/e2e/test_021_base_flash_screen.py b/e2e/test_021_base_flash_screen.py new file mode 100644 index 00000000..262bd2b2 --- /dev/null +++ b/e2e/test_021_base_flash_screen.py @@ -0,0 +1,118 @@ +from unittest.mock import patch, MagicMock, call +from kivy.base import EventLoop, EventLoopBase +from kivy.tests.common import GraphicUnitTest +from src.app.screens.base_flash_screen import BaseFlashScreen + + +class TestBaseFlashScreen(GraphicUnitTest): + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.App.get_running_app") + def test_init(self, mock_get_running_app): + screen = BaseFlashScreen(wid="mock_screen", name="MockScreen") + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + # default assertions + self.assertEqual(grid.id, "mock_screen_grid") + self.assertEqual(len(grid.children), 0) + self.assertEqual(screen.firmware, None) + self.assertEqual(screen.baudrate, None) + self.assertEqual(screen.thread, None) + self.assertEqual(screen.trigger, None) + self.assertEqual(screen.output, None) + + # patch assertions + mock_get_running_app.assert_has_calls( + [call().config.get("locale", "lang")], any_order=True + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.App.get_running_app") + @patch("src.app.screens.base_flash_screen.os.path.exists", side_effect=[True]) + def test_set_firmware(self, mock_exists, mock_get_running_app): + screen = BaseFlashScreen(wid="mock_screen", name="MockScreen") + screen.firmware = "mock.kfpkg" + self.assertEqual(screen.firmware, "mock.kfpkg") + + mock_exists.assert_called_once_with("mock.kfpkg") + + # patch assertions + mock_get_running_app.assert_has_calls( + [call().config.get("locale", "lang")], any_order=True + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.App.get_running_app") + @patch("src.app.screens.base_flash_screen.os.path.exists", side_effect=[False]) + def test_fail_set_firmware(self, mock_exists, mock_get_running_app): + screen = BaseFlashScreen(wid="mock_screen", name="MockScreen") + + with self.assertRaises(ValueError) as exc_info: + screen.firmware = "mock.kfpkg" + + self.assertEqual(str(exc_info.exception), "Firmware not exist: mock.kfpkg") + # patch assertions + mock_get_running_app.assert_has_calls( + [call().config.get("locale", "lang")], any_order=True + ) + + mock_exists.assert_called_once_with("mock.kfpkg") + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.App.get_running_app") + def test_set_baudrate(self, mock_get_running_app): + screen = BaseFlashScreen(wid="mock_screen", name="MockScreen") + screen.baudrate = "1500000" + self.assertEqual(screen.baudrate, "1500000") + # patch assertions + mock_get_running_app.assert_has_calls( + [call().config.get("locale", "lang")], any_order=True + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.App.get_running_app") + def test_set_thread(self, mock_get_running_app): + screen = BaseFlashScreen(wid="mock_screen", name="MockScreen") + screen.thread = MagicMock() + screen.thread.start = MagicMock() + + screen.thread.start() + screen.thread.start.assert_called_once() + # patch assertions + mock_get_running_app.assert_has_calls( + [call().config.get("locale", "lang")], any_order=True + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.App.get_running_app") + @patch("src.app.screens.base_flash_screen.Clock.create_trigger") + def test_set_trigger(self, mock_create_trigger, mock_get_running_app): + mock_trigger = MagicMock() + screen = BaseFlashScreen(wid="mock_screen", name="MockScreen") + screen.trigger = mock_trigger + + # patch assertions + mock_get_running_app.assert_has_calls( + [call().config.get("locale", "lang")], any_order=True + ) + mock_create_trigger.assert_has_calls([call(mock_trigger)], any_order=True) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.App.get_running_app") + def test_set_output(self, mock_get_running_app): + screen = BaseFlashScreen(wid="mock_screen", name="MockScreen") + screen.output = ["mock", "this", "test"] + self.assertEqual(len(screen.output), 3) + # patch assertions + mock_get_running_app.assert_has_calls( + [call().config.get("locale", "lang")], any_order=True + ) diff --git a/e2e/test_022_flash_screen.py b/e2e/test_022_flash_screen.py new file mode 100644 index 00000000..be139452 --- /dev/null +++ b/e2e/test_022_flash_screen.py @@ -0,0 +1,494 @@ +import sys +from unittest.mock import patch, MagicMock, call +from kivy.base import EventLoop, EventLoopBase +from kivy.tests.common import GraphicUnitTest +from src.app.screens.flash_screen import FlashScreen + + +class TestFlashScreen(GraphicUnitTest): + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.App.get_running_app") + @patch("src.app.screens.flash_screen.partial") + @patch("src.app.screens.flash_screen.Clock.schedule_once") + def test_init(self, mock_schedule_once, mock_partial, mock_get_running_app): + screen = FlashScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + # default assertions + self.assertEqual(grid.id, "flash_screen_grid") + self.assertEqual(len(grid.children), 0) + self.assertEqual(screen.firmware, None) + self.assertEqual(screen.baudrate, None) + self.assertEqual(screen.thread, None) + self.assertEqual(screen.trigger, None) + self.assertEqual(screen.output, None) + + # patch assertions + mock_get_running_app.assert_has_calls( + [call().config.get("locale", "lang")], any_order=True + ) + mock_partial.assert_called_once_with( + screen.update, name=screen.name, key="canvas" + ) + mock_schedule_once.assert_has_calls([call(mock_partial(), 0)], any_order=True) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.App.get_running_app") + def test_fail_update_wrong_name(self, mock_get_running_app): + screen = FlashScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + with self.assertRaises(ValueError) as exc_info: + screen.update(name="MockScreen") + + self.assertEqual(str(exc_info.exception), "Invalid screen name: MockScreen") + + # patch assertions + mock_get_running_app.assert_has_calls( + [call().config.get("locale", "lang")], any_order=True + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update_wrong_key(self, mock_redirect_error, mock_get_locale): + screen = FlashScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + screen.update(name=screen.name, key="mock") + + # patch assertions + mock_get_locale.assert_any_call() + mock_redirect_error.assert_called_once_with('Invalid key: "mock"') + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.App.get_running_app") + def test_update_locale(self, mock_get_running_app): + screen = FlashScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + screen.update(name=screen.name, key="locale", value="en_US.UTF8") + + self.assertEqual(screen.locale, "en_US.UTF8") + + # patch assertions + mock_get_running_app.assert_has_calls( + [call().config.get("locale", "lang")], any_order=True + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.App.get_running_app") + @patch("src.app.screens.flash_screen.Rectangle") + @patch("src.app.screens.flash_screen.Color") + def test_update_canvas(self, mock_color, mock_rectangle, mock_get_running_app): + screen = FlashScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + # patch assertions + mock_get_running_app.assert_has_calls( + [call().config.get("locale", "lang")], any_order=True + ) + mock_color.assert_called_once_with(0, 0, 0, 1) + + # Check why the below happens: In linux, it will set window + # dimension to 640, 800. In Mac, it will set window 1280, 1600 + args, kwargs = mock_rectangle.call_args_list[-1] + self.assertTrue("size" in kwargs) + self.assertEqual(len(args), 0) + mock_rectangle.assert_called_once_with(size=window.size) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.App.get_running_app") + def test_update_baudrate(self, mock_get_running_app): + screen = FlashScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + screen.update(name=screen.name, key="baudrate", value=1500000) + + self.assertEqual(screen.baudrate, 1500000) + + # patch assertions + mock_get_running_app.assert_has_calls( + [call().config.get("locale", "lang")], any_order=True + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.App.get_running_app") + @patch("src.utils.flasher.base_flasher.os.path.exists", side_effect=[True]) + def test_update_firmware(self, mock_exists, mock_get_running_app): + screen = FlashScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + screen.update(name=screen.name, key="firmware", value="mock.kfpkg") + + self.assertEqual(screen.firmware, "mock.kfpkg") + mock_exists.assert_called_once_with("mock.kfpkg") + + # patch assertions + mock_get_running_app.assert_has_calls( + [call().config.get("locale", "lang")], any_order=True + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.App.get_running_app") + @patch("src.utils.flasher.base_flasher.os.path.exists", side_effect=[True, True]) + def test_update_flasher(self, mock_exists, mock_get_running_app): + screen = FlashScreen() + screen.firmware = "mock.kfpkg" + screen.baudrate = 1500000 + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + screen.update(name=screen.name, key="flasher") + + self.assertEqual(screen.flasher.firmware, "mock.kfpkg") + self.assertEqual(screen.flasher.baudrate, 1500000) + + # patch assertions + mock_get_running_app.assert_has_calls( + [call().config.get("locale", "lang")], any_order=True + ) + mock_exists.assert_has_calls([call("mock.kfpkg"), call("mock.kfpkg")]) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.App.get_running_app") + def test_on_pre_enter(self, mock_get_running_app): + screen = FlashScreen() + screen.on_pre_enter() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + self.assertTrue(hasattr(FlashScreen, "on_print_callback")) + self.assertTrue(hasattr(FlashScreen, "on_process_callback")) + self.assertTrue(hasattr(FlashScreen, "on_trigger_callback")) + self.assertIn(f"{screen.id}_subgrid", screen.ids) + self.assertIn(f"{screen.id}_loader", screen.ids) + self.assertIn(f"{screen.id}_progress", screen.ids) + self.assertIn(f"{screen.id}_info", screen.ids) + + # patch assertions + mock_get_running_app.assert_has_calls( + [call().config.get("locale", "lang")], any_order=True + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.App.get_running_app") + def test_on_print_callback(self, mock_get_running_app): + screen = FlashScreen() + screen.output = [] + screen.on_pre_enter() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + on_print_callback = getattr(FlashScreen, "on_print_callback") + on_print_callback("[color=#00ff00] INFO [/color] mock") + + self.assertEqual(screen.output, ["[color=#00ff00] INFO [/color] mock"]) + # patch assertions + mock_get_running_app.assert_has_calls( + [call().config.get("locale", "lang")], any_order=True + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.App.get_running_app") + def test_on_print_callback_programming_bin(self, mock_get_running_app): + screen = FlashScreen() + screen.output = [] + screen.on_pre_enter() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + on_print_callback = getattr(FlashScreen, "on_print_callback") + + # Let's "print" some previous infos + for i in range(19): + on_print_callback(f"[color=#00ff00] INFO [/color] mock test message {i}") + + self.assertEqual(len(screen.output), 18) + + # Now print programming BIN + on_print_callback("Programming BIN: |=----------| 0.21% at 21 KiB/s") + self.assertEqual( + screen.output[-1], "Programming BIN: |=----------| 0.21% at 21 KiB/s" + ) + + # patch assertions + mock_get_running_app.assert_has_calls( + [call().config.get("locale", "lang")], any_order=True + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.App.get_running_app") + def test_on_print_callback_separator(self, mock_get_running_app): + screen = FlashScreen() + screen.output = [] + screen.on_pre_enter() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + on_print_callback = getattr(FlashScreen, "on_print_callback") + + # Let's "print" some previous infos + for i in range(19): + on_print_callback(f"[color=#00ff00] INFO [/color] mock test message {i}") + + self.assertEqual(len(screen.output), 18) + + # Now print programming BIN + on_print_callback("*") + self.assertEqual(screen.output[-2], "*") + self.assertEqual(screen.output[-1], "") + + # patch assertions + mock_get_running_app.assert_has_calls( + [call().config.get("locale", "lang")], any_order=True + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.App.get_running_app") + def test_on_print_callback_message_not_recognized(self, mock_get_running_app): + screen = FlashScreen() + screen.output = [] + screen.on_pre_enter() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + on_print_callback = getattr(FlashScreen, "on_print_callback") + + # Let's "print" some previous infos + + warn = "[WARN] mock test" + on_print_callback(warn) + + # patch assertions + mock_get_running_app.assert_has_calls( + [call().config.get("locale", "lang")], any_order=True + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.App.get_running_app") + def test_on_print_callback_pop_ouput(self, mock_get_running_app): + screen = FlashScreen() + screen.output = [] + screen.on_pre_enter() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + on_print_callback = getattr(FlashScreen, "on_print_callback") + + for i in range(19): + on_print_callback(f"[color=#00ff00] INFO [/color] mock test message {i}") + + self.assertEqual(len(screen.output), 18) + + # patch assertions + mock_get_running_app.assert_has_calls( + [call().config.get("locale", "lang")], any_order=True + ) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.App.get_running_app") + @patch("src.app.screens.flash_screen.FlashScreen.trigger") + def test_on_print_callback_rebooting(self, mock_trigger, mock_get_running_app): + screen = FlashScreen() + screen.output = [] + screen.on_pre_enter() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + on_print_callback = getattr(FlashScreen, "on_print_callback") + on_print_callback("[color=#00ff00] INFO [/color] Rebooting...\n") + + self.assertEqual( + screen.output, ["[color=#00ff00] INFO [/color] Rebooting...\n"] + ) + # patch assertions + mock_get_running_app.assert_has_calls( + [call().config.get("locale", "lang")], any_order=True + ) + mock_trigger.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_on_process_callback(self, mock_get_locale): + screen = FlashScreen() + screen.output = [] + screen.on_pre_enter() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + if sys.platform in ("linux", "win32"): + sizes = [screen.SIZE_M, screen.SIZE_MP, screen.SIZE_P] + + else: + sizes = [screen.SIZE_MM, screen.SIZE_M, screen.SIZE_MP] + + text = "".join( + [ + f"[size={sizes[1]}sp][b]PLEASE DO NOT UNPLUG YOUR DEVICE[/b][/size]", + "\n", + f"[size={sizes[0]}sp]4.76 %[/size]", + "\n", + f"[size={sizes[2]}sp]", + "Flashing ", + "[color=#efcc00][b]firmware.bin[/b][/color] at ", + "[color=#efcc00][b]21 KiB/s[/b][/color]", + "[/size]", + ] + ) + on_process_callback = getattr(FlashScreen, "on_process_callback") + on_process_callback( + file_type="firmware.bin", iteration=1, total=21, suffix="21 KiB/s" + ) + + self.assertEqual(screen.ids[f"{screen.id}_progress"].text, text) + # patch assertions + + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_on_trigger_callback(self, mock_get_locale): + screen = FlashScreen() + screen.output = [] + screen.on_pre_enter() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + if sys.platform in ("linux", "win32"): + size = screen.SIZE_M + + else: + size = screen.SIZE_M + + text = "".join( + [ + f"[size={size}sp][b]DONE![/b][/size]", + "\n", + "\n", + f"[size={size}sp]", + "[color=#00FF00]", + "[ref=Back][u]Back[/u][/ref]", + "[/color]", + " ", + "[color=#EFCC00]", + "[ref=Quit][u]Quit[/u][/ref]", + "[/color]", + ] + ) + + on_trigger_callback = getattr(FlashScreen, "on_trigger_callback") + on_trigger_callback(0) + + self.assertEqual(screen.ids[f"{screen.id}_progress"].text, text) + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_on_enter(self, mock_redirect_error, mock_get_locale): + screen = FlashScreen() + screen.on_pre_enter() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + screen.on_enter() + + # patch assertions + mock_get_locale.assert_any_call() + mock_redirect_error.assert_called_once_with("Flasher isnt configured") + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.App.get_running_app") + @patch("src.app.screens.flash_screen.partial") + @patch("src.app.screens.flash_screen.threading.Thread") + @patch("src.utils.flasher.Flasher") + def test_on_enter( + self, mock_flasher, mock_thread, mock_partial, mock_get_running_app + ): + mock_flasher.__class__.print_callback = MagicMock() + + screen = FlashScreen() + screen.flasher = MagicMock() + screen.flasher.ktool = MagicMock() + screen.flasher.flash = MagicMock() + + screen.on_pre_enter() + screen.on_enter() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # prepare assertions + on_process_callback = getattr(FlashScreen, "on_process_callback") + + # patch assertions + mock_get_running_app.assert_has_calls( + [call().config.get("locale", "lang")], any_order=True + ) + mock_partial.assert_has_calls( + [ + call(screen.update, name=screen.name, key="canvas"), + call(screen.flasher.flash, callback=on_process_callback), + ], + any_order=True, + ) + mock_thread.assert_called_once_with(name=screen.name, target=mock_partial()) diff --git a/e2e/test_023_wipe_screen.py b/e2e/test_023_wipe_screen.py new file mode 100644 index 00000000..7ce6f4a0 --- /dev/null +++ b/e2e/test_023_wipe_screen.py @@ -0,0 +1,339 @@ +import sys +import os +from unittest.mock import patch, MagicMock, call +from kivy.base import EventLoop, EventLoopBase +from kivy.tests.common import GraphicUnitTest +from kivy.core.text import LabelBase, DEFAULT_FONT +from src.app.screens.wipe_screen import WipeScreen + + +class TestWipeScreen(GraphicUnitTest): + + @classmethod + def setUpClass(cls): + cwd_path = os.path.dirname(__file__) + rel_assets_path = os.path.join(cwd_path, "..", "assets") + assets_path = os.path.abspath(rel_assets_path) + noto_sans_path = os.path.join(assets_path, "NotoSansCJK_Cy_SC_KR_Krux.ttf") + LabelBase.register(DEFAULT_FONT, noto_sans_path) + + @classmethod + def teardown_class(cls): + EventLoop.exit() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.BaseScreen.get_locale") + @patch("src.app.screens.wipe_screen.partial") + @patch("src.app.screens.wipe_screen.Clock.schedule_once") + def test_init(self, mock_schedule_once, mock_partial, mock_get_locale): + screen = WipeScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + grid = window.children[0].children[0] + + # default assertions + self.assertEqual(grid.id, "wipe_screen_grid") + self.assertEqual(len(grid.children), 0) + self.assertEqual(screen.baudrate, None) + self.assertEqual(screen.thread, None) + self.assertEqual(screen.trigger, None) + self.assertEqual(screen.output, None) + + # patch assertions + mock_get_locale.assert_called_once() + mock_partial.assert_called_once_with( + screen.update, name=screen.name, key="canvas" + ) + mock_schedule_once.assert_has_calls([call(mock_partial(), 0)], any_order=True) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_update_wrong_name(self, mock_redirect_error, mock_get_locale): + screen = WipeScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + screen.update(name="MockScreen") + + # patch assertions + mock_get_locale.assert_called_once() + mock_redirect_error.assert_called_once_with("Invalid screen name: MockScreen") + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.BaseScreen.get_locale") + def test_fail_update_wrong_key(self, mock_get_locale): + screen = WipeScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + with self.assertRaises(ValueError) as exc_info: + screen.update(name=screen.name, key="mock") + + self.assertEqual(str(exc_info.exception), 'Invalid key: "mock"') + + # patch assertions + mock_get_locale.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.BaseScreen.get_locale") + def test_update_locale(self, mock_get_locale): + screen = WipeScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + screen.update(name=screen.name, key="locale", value="en_US.UTF8") + + self.assertEqual(screen.locale, "en_US.UTF8") + + # patch assertions + mock_get_locale.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.BaseScreen.get_locale") + @patch("src.app.screens.wipe_screen.Rectangle") + @patch("src.app.screens.wipe_screen.Color") + def test_update_canvas(self, mock_color, mock_rectangle, mock_get_locale): + screen = WipeScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + window = EventLoop.window + + # patch assertions + mock_get_locale.assert_called_once() + mock_color.assert_called_once_with(0, 0, 0, 1) + + # Check why the below happens: In linux, it will set window + # dimension to 640, 800. In Mac, it will set window 1280, 1600 + args, kwargs = mock_rectangle.call_args_list[-1] + self.assertTrue("size" in kwargs) + self.assertEqual(len(args), 0) + mock_rectangle.assert_called_once_with(size=window.size) + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_update_device(self, mock_get_locale): + screen = WipeScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + screen.update(name=screen.name, key="device", value="amigo") + + self.assertEqual(screen.device, "amigo") + + # patch assertions + mock_get_locale.asset_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch("src.app.screens.base_screen.BaseScreen.get_locale") + def test_update_wiper(self, mock_get_locale): + screen = WipeScreen() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + screen.update(name=screen.name, key="wiper", value=1500000) + self.assertEqual(screen.wiper.baudrate, 1500000) + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_on_pre_enter(self, mock_get_locale): + screen = WipeScreen() + screen.on_pre_enter() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + self.assertTrue(hasattr(WipeScreen, "on_print_callback")) + self.assertTrue(hasattr(WipeScreen, "on_trigger_callback")) + self.assertIn(f"{screen.id}_subgrid", screen.ids) + self.assertIn(f"{screen.id}_loader", screen.ids) + self.assertIn(f"{screen.id}_progress", screen.ids) + self.assertIn(f"{screen.id}_info", screen.ids) + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_on_print_callback(self, mock_get_locale): + screen = WipeScreen() + screen.output = [] + screen.on_pre_enter() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + on_print_callback = getattr(WipeScreen, "on_print_callback") + on_print_callback("[color=#00ff00] INFO [/color] mock") + + self.assertEqual(screen.output, ["[color=#00ff00] INFO [/color] mock"]) + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_on_print_callback_pop_ouput(self, mock_get_locale): + screen = WipeScreen() + screen.output = [] + screen.on_pre_enter() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + on_print_callback = getattr(WipeScreen, "on_print_callback") + + for i in range(19): + on_print_callback(f"[color=#00ff00] INFO [/color] mock test message {i}") + + self.assertEqual(len(screen.output), 18) + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.wipe_screen.WipeScreen.trigger") + def test_on_print_callback_erased(self, mock_trigger, mock_get_locale): + screen = WipeScreen() + screen.output = [] + screen.on_pre_enter() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + on_print_callback = getattr(WipeScreen, "on_print_callback") + on_print_callback("[color=#00ff00] INFO [/color] SPI Flash erased.") + + self.assertEqual( + screen.output, ["[color=#00ff00] INFO [/color] SPI Flash erased."] + ) + # patch assertions + mock_get_locale.assert_any_call() + mock_trigger.assert_called_once() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + def test_on_trigger_callback(self, mock_get_locale): + screen = WipeScreen() + screen.output = [] + screen.on_pre_enter() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + if sys.platform in ("linux", "win32"): + size = screen.SIZE_M + + else: + size = screen.SIZE_M + + text = "".join( + [ + f"[size={size}sp][b]DONE![/b][/size]", + "\n", + f"[size={size}sp]", + "[color=#00FF00]", + "[ref=Back][u]Back[/u][/ref]", + "[/color]", + " ", + "[color=#EFCC00]", + "[ref=Quit][u]Quit[/u][/ref]", + "[/color]", + ] + ) + on_trigger_callback = getattr(WipeScreen, "on_trigger_callback") + on_trigger_callback(0) + + self.assertEqual(screen.ids[f"{screen.id}_progress"].text, text) + self.assertEqual(screen.ids[f"{screen.id}_progress"].text, text) + + # patch assertions + mock_get_locale.assert_any_call() + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.base_screen.BaseScreen.redirect_error") + def test_fail_on_enter(self, mock_redirect_error, mock_get_locale): + screen = WipeScreen() + screen.on_pre_enter() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + screen.on_enter() + + # patch assertions + mock_get_locale.assert_any_call() + mock_redirect_error.assert_called_once_with("Wiper isnt configured") + + @patch.object(EventLoopBase, "ensure_window", lambda x: None) + @patch( + "src.app.screens.base_screen.BaseScreen.get_locale", return_value="en_US.UTF-8" + ) + @patch("src.app.screens.wipe_screen.partial") + @patch("src.app.screens.wipe_screen.threading.Thread") + @patch("src.utils.flasher.Flasher") + def test_on_enter(self, mock_flasher, mock_thread, mock_partial, mock_get_locale): + mock_flasher.__class__.print_callback = MagicMock() + + screen = WipeScreen() + screen.flasher = MagicMock() + screen.flasher.ktool = MagicMock() + screen.flasher.flash = MagicMock() + + screen.update(name=screen.name, key="device", value="amigo") + screen.update(name=screen.name, key="wiper", value=1500000) + screen.on_pre_enter() + screen.on_enter() + self.render(screen) + + # get your Window instance safely + EventLoop.ensure_window() + + # patch assertions + mock_get_locale.assert_any_call() + mock_partial.assert_has_calls( + [ + call(screen.update, name=screen.name, key="canvas"), + call(screen.wiper.wipe, device="amigo"), + ], + any_order=True, + ) + mock_thread.assert_called_once_with(name=screen.name, target=mock_partial()) diff --git a/electron-builder.json5 b/electron-builder.json5 deleted file mode 100644 index c34cceea..00000000 --- a/electron-builder.json5 +++ /dev/null @@ -1,53 +0,0 @@ -/** - * @see https://www.electron.build/configuration/configuration - */ -{ - "$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json", - "appId": "org.selfcustody.krux-installer", - "productName": "krux-installer", - "asar": true, - "directories": { - "output": "release/${version}" - }, - "files": [ - "dist-electron", - "dist" - ], - "extraResources": [ - "extraResources" - ], - "mac": { - "artifactName": "${productName}_${version}.${ext}", - "target": [ - "dmg" - ] - }, - "linux": { - "category": "Utility", - "icon": "public/icon.png", - "desktop": { - "Icon": "/usr/share/icons/hicolor/0x0/apps/krux-installer.png", - "Keywords": "electron;krux;vite;vuetify;vue3;vue", - "Terminal": false - }, - "target": ["AppImage", "deb", "rpm"] - }, - "win": { - "icon": "public/icon.png", - "target": [ - { - "target": "nsis", - "arch": [ - "x64" - ] - } - ], - "artifactName": "${productName}_${version}.${ext}" - }, - "nsis": { - "oneClick": false, - "perMachine": false, - "allowToChangeInstallationDirectory": true, - "deleteAppDataOnUninstall": false - } -} diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts deleted file mode 100644 index 72a6228c..00000000 --- a/electron/electron-env.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/// - -declare namespace NodeJS { - interface ProcessEnv { - VSCODE_DEBUG?: 'true' - DIST_ELECTRON: string - DIST: string - /** /dist/ or /public/ */ - PUBLIC: string - } -} \ No newline at end of file diff --git a/electron/main/index.ts b/electron/main/index.ts deleted file mode 100644 index 65630b17..00000000 --- a/electron/main/index.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { createRequire } from 'module' -import App from '../../lib/app' -import Storage from '../../lib/storage' -import ChangePageHandler from '../../lib/change-page' -import DownloadResourcesHandler from '../../lib/download-resources' -import CheckResourcesHandler from '../../lib/check-resource' -import UnzipResourceHandler from '../../lib/unzip-resource' -import VerifyOfficialReleasesFetchHandler from '../../lib/verify-official-releases-fetch' -import VerifyOfficialReleasesHashHandler from '../../lib/verify-official-releases-hash' -import VerifyOfficialReleasesSignHandler from '../../lib/verify-official-releases-sign' -import StoreSetHandler from '../../lib/store-set' -import StoreGetHandler from '../../lib/store-get' -import VerifyOpensslHandler from '../../lib/verify-openssl' -import CheckIfItWillFlashHandler from '../../lib/check-if-it-will-flash' -import CheckIfItWillWipeHandler from '../../lib/check-if-it-will-wipe' -import FlashHandler from '../../lib/flash' -import QuitHandler from '../../lib/quit' -import WipeHandler from '../../lib/wipe' - -const { version } = createRequire(import.meta.url)('../../package.json') -const kruxInstaller = new App(`KruxInstaller | v${version}`) - -kruxInstaller.start(async ({ app, win, ipcMain}) => { - // Create storage - const storageBuilder = new Storage(app) - app.store = await storageBuilder.build() - - // Reset configurations - app.store.set('device', 'Select device') - app.store.set('version', 'Select version') - app.store.set('versions', []) - - // Create download resource handler - const changePage = new ChangePageHandler(win, app.store, ipcMain) - changePage.build() - - // Create download resource handler - const downloadResource = new DownloadResourcesHandler(win, app.store, ipcMain) - downloadResource.build() - - // Create check resource handler - const checkResource = new CheckResourcesHandler(win, app.store, ipcMain) - checkResource.build() - - // Create unzip resource handler - const unzipResource = new UnzipResourceHandler(win, app.store, ipcMain) - unzipResource.build() - - // Create fetcher for newest official release handler - const verifyOfficialReleasesFetch = new VerifyOfficialReleasesFetchHandler(win, app.store, ipcMain) - verifyOfficialReleasesFetch.build() - - // Create handler for official release sha256.txt - const verifyOfficialReleasesHash = new VerifyOfficialReleasesHashHandler(win, app.store, ipcMain) - verifyOfficialReleasesHash.build() - - // Create handler for official release .sig and .pem handler - const verifyOfficialReleasesSign = new VerifyOfficialReleasesSignHandler(win, app.store, ipcMain) - verifyOfficialReleasesSign.build() - - // Create handler for verify existence of openssl - const verifyOpenssl = new VerifyOpensslHandler(win, app.store, ipcMain) - verifyOpenssl.build() - - // Create store setter handler - const storeSet = new StoreSetHandler(win, app.store, ipcMain) - storeSet.build() - - // Create store getter handler - const storeGet = new StoreGetHandler(win, app.store, ipcMain) - storeGet.build() - - // Create 'check if it will flash handler - const checkIfItWillFlashHandler = new CheckIfItWillFlashHandler(win, app.store, ipcMain) - checkIfItWillFlashHandler.build() - - // Create 'check if it will wipe handler - const checkIfItWillWipeHandler = new CheckIfItWillWipeHandler(win, app.store, ipcMain) - checkIfItWillWipeHandler.build() - - // Create 'flash' handler - const flashHandler = new FlashHandler(win, app.store, ipcMain) - flashHandler.build() - - // Create 'flash' handler - const wipeHandler = new WipeHandler(win, app.store, ipcMain) - wipeHandler.build() - - // Create 'quit' handler - const quitHandler = new QuitHandler(win, app.store, ipcMain) - quitHandler.build() - - // Create Wdio test handlers - // if environment variable WDIO_ELECTRON equals 'true' - if (process.env.NODE_ENV === 'test') { - const _electron = await import('electron') - ipcMain.handle("wdio-electron.execute", (_, script, args) => { - return new Function(`return (${script}).apply(this, arguments)`)(_electron, ...args); - }) - } -}) diff --git a/electron/preload/index.ts b/electron/preload/index.ts deleted file mode 100644 index 4b0551e6..00000000 --- a/electron/preload/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -const { contextBridge, ipcRenderer } = require('electron') - -ipcRenderer.on('main-process-message', (_event, args) => { - console.log(args) -}) - -if (process.env.NODE_ENV === 'test') { - contextBridge.exposeInMainWorld('wdioElectron', { - execute: (script, args) => ipcRenderer.invoke("wdio-electron.execute", script, args) - }) -} - -contextBridge.exposeInMainWorld('api', { - async invoke (channel: string, data: string): Promise { - await ipcRenderer.invoke(channel, data) - }, - onData (channel: string, callback: any): void { - ipcRenderer.on(`${channel}:data`, callback) - }, - onSuccess (channel: string, callback: any): void { - ipcRenderer.on(`${channel}:success`, callback) - }, - onceSuccess (channel: string, callback: any): void { - ipcRenderer.once(`${channel}:success`, callback) - }, - onError (channel: string, callback: any): void { - ipcRenderer.on(`${channel}:error`, callback) - } -}) diff --git a/images/vscodium.png b/images/vscodium.png deleted file mode 100644 index dbb72525..00000000 Binary files a/images/vscodium.png and /dev/null differ diff --git a/images/vscodium_debug.png b/images/vscodium_debug.png deleted file mode 100644 index eec8d702..00000000 Binary files a/images/vscodium_debug.png and /dev/null differ diff --git a/index.html b/index.html deleted file mode 100644 index 075ba320..00000000 --- a/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - <%= title %> - - -
- - - diff --git a/krux-installer.py b/krux-installer.py new file mode 100644 index 00000000..fef36936 --- /dev/null +++ b/krux-installer.py @@ -0,0 +1,29 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +krux-installer.py +""" + +if __name__ == "__main__": + from src.app import KruxInstallerApp + + app = KruxInstallerApp() + app.run() diff --git a/lib/app.ts b/lib/app.ts deleted file mode 100644 index 176f0bad..00000000 --- a/lib/app.ts +++ /dev/null @@ -1,282 +0,0 @@ -/// - -import { release } from 'node:os' -import { dirname, join } from 'node:path' -import { access } from 'node:fs/promises' -import { app, BrowserWindow, shell, ipcMain } from 'electron' -import Base from './base' - -/** - * Extend `Base` class for initializing KruxInstaller application, - * assigning `krux:app` to `name`property. - * - * Once `start` method is called, it returns the following objects: - * - * - `app: Electron.App`; - * - `ipcMain: Electron.IpcMain` - * - `win: Electron.BrowserWindow` - * - * @example - * ``` - * import { version } from '../package.json' - * const myapp = new App(`MyApp | v${version}`) - * myapp.start(() =>) - * ``` - * @see Electron.App - * @see Electron.IpcMain - * @see Electron.BrowserWindow - */ -export default class App extends Base { - - /** - * The window title - */ - private title: string; - - constructor (title: string) { - super('krux:app') - this.title = title - this.setupEnvironment() - this.setupOpenssl() - this.setupRelease() - this.setupNotifications() - this.setupSingleInstanceLock() - } - - /** - * Setup environment variables specific to application: - * - `DIST`: where renderer distribuition files are placed - * - `DIST_ELECTRON`: where main process distribuition files are placed - * - `PUBLIC`: where "public" files are placed - * - `WDIO_ELECTRON`: if the context is for test or not - */ - setupEnvironment (): void { - /* The built directory structure - * - * ├─┬ dist-electron - * │ ├─┬ main - * │ │ └── index.js > Electron-Main - * │ └─┬ preload - * │ └── index.js > Preload-Scripts - * ├─┬ dist - * │ └── index.html > Electron-Renderer - */ - process.env.DIST_ELECTRON = join(__dirname, '..') - process.env.DIST = join(process.env.DIST_ELECTRON, '../dist') - process.env.PUBLIC = process.env.VITE_DEV_SERVER_URL - ? join(process.env.DIST_ELECTRON, '../public') - : process.env.DIST - - // Remove electron security warnings - // This warning only shows in development mode - // Read more on https://www.electronjs.org/docs/latest/tutorial/security - // process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true' - - this.log('Application environment') - this.log(` PORTABLE_EXECUTABLE_FILE: : ${process.env.PORTABLE_EXECUTABLE_FILE}`) - this.log(` DIST : ${process.env.DIST}`) - this.log(` DIST_ELECTRON : ${process.env.DIST_ELECTRON}`) - this.log(` PUBLIC : ${process.env.PUBLIC}`) - this.log(` TEST : ${process.env.TEST}`) - this.log(` ELECTRON_DISABLE_SECURITY_WARNINGS: ${process.env.DIST}`) - } - - /** - * This will disable GPU Acceleration for Windows 7 - */ - private setupRelease (): void { - if (release().startsWith('6.1')) { - this.log('Disabling GPU Acceleration') - app.disableHardwareAcceleration() - } - } - - /** - * Set application name for Windows 10+ notifications - */ - private setupNotifications (): void { - if (process.platform === 'win32') { - this.log('Setting application name for Windows notifications') - app.setAppUserModelId(app.getName()) - } - } - - /** - * Setup single instance lock - */ - private setupSingleInstanceLock (): void { - if (!app.requestSingleInstanceLock()) { - this.log('Requesting single instance lock') - app.quit() - process.exit(0) - } - } - - /** - * This path only will exist when build occurs - * inside github-actions, once OpenSSL is built on runtime - * @param _env - * @param openssls - */ - private async setupBuiltinOpensslWin32 (_env: string[], openssls: string[], possiblePaths: string[]): Promise { - for (let i in possiblePaths) { - try { - this.debug(` trying ${possiblePaths[i]}`) - await access(possiblePaths[i]) - if (_env.indexOf(possiblePaths[i]) === -1) { - openssls.push(possiblePaths[i]) - } - break - } catch (error) { - this.debug(` OPENSSL ADD PATH WARN: ${error}`) - } - } - } - - /** - * Check if platform (darwin or win32) - * needs and additional configuration - * to add openssl binary - */ - private async setupOpenssl (): Promise { - this.log(`Adding openssl in ${process.platform} environment variable PATH`) - const openssls = [] - let separator = '' - - if (process.platform === 'linux') { - this.log(' no need for add') - } else if (process.platform === 'darwin' ) { - separator = ':' - const _env = (process.env.PATH as string).split(separator) - if (_env.indexOf('/usr/local/opt/openssl/bin') === -1) { - openssls.push('/usr/local/opt/openssl/bin') - } - if (_env.indexOf('/System/Library/OpenSSL') === -1) { - openssls.push('/System/Library/OpenSSL') - } - } else if (process.platform === 'win32') { - separator = ';' - const _env = (process.env.PATH as string).split(separator) - await this.setupBuiltinOpensslWin32(_env, openssls, [ - join(process.env.DIST, '..', 'release', 'extraResources', 'OpenSSL', 'bin'), - join(process.env.DIST, '..', '..', 'extraResources', 'OpenSSL', 'bin'), - join(app.getPath('appData'), '..', 'Local', 'Programs', 'krux-installer', 'resources', 'extraResources', 'OpenSSL', 'bin'), - join(process.env.ProgramFiles, 'Git', 'usr', 'bin'), - join(process.env.ProgramFiles, 'OpenVPN', 'bin'), - ]); - } - for (let i in openssls) { - this.log(` adding ${openssls[i]} to PATH`) - process.env.PATH += `${separator}${openssls[i]}` - } - } - - /** - * Configure app listeners: - * - `app.on('window-all-closed')`; - * - `app.on('activate')`; - * - `app.on('second-instance')`; - * - * And invoke `this.create` when app is ready. - * @returns KruxInstaller.StartedApp - */ - public start (callback: any): void { - app.on('window-all-closed', () => { - console.log('All windows closed: quiting') - if (process.platform !== 'darwin') app.quit() - }) - - app.on('activate', () => { - this.log('Checking existence of other windows') - const allWindows = BrowserWindow.getAllWindows() - if (allWindows.length) { - allWindows[0].focus() - } else { - this.create() - } - }) - - app.on('second-instance', () => { - this.log('Trying to opening a second instance') - if (this.win) { - // Focus on the main window if the user tried to open another - if (this.win.isMinimized()) { - this.win.restore() - } - this.win.focus() - } - }) - - app.whenReady().then(() => { - this.log('App ready') - this.create() - callback({ app: app, win: this.win, ipcMain: ipcMain }) - }) - } - - /** - * Create application when ready. Do not all it directly. - * @see start - */ - private create (): void { - this.log('Creating app') - const preload = join(__dirname, '../preload/index.js') - const url = process.env.VITE_DEV_SERVER_URL - const indexHtml = join(process.env.DIST, 'index.html') - const icon = join(process.env.PUBLIC, 'favicon.ico') - - this.log('Application variables') - this.log(` preload : ${preload}`) - this.log(` url : ${url}`) - this.log(` index.html: ${indexHtml}`) - this.log(` title : ${this.title}`) - this.log(` icon : ${icon}`) - - this.log('Creating Browser Window') - this.win = new BrowserWindow({ - width: 880, - height: 880, - title: this.title, - icon: icon, - show: false, - webPreferences: { - preload, - // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production - // Consider using contextBridge.exposeInMainWorld - // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation - nodeIntegration: false, - contextIsolation: true, - }, - }) - - if (process.env.VITE_DEV_SERVER_URL) { // electron-vite-vue#298 - this.log(`loading ${url}`) - this.win.loadURL(url) - // Open devTool if the app is not packaged - this.win.webContents.openDevTools() - } else { - this.log(`loading ${indexHtml}`) - this.win.loadFile(indexHtml) - } - - // Test actively push message to the Electron-Renderer - this.win.webContents.on('did-finish-load', () => { - this.log('Finished loading') - const msg = `${this.title} running` - this.log(msg) - }) - - // Make all links open with the browser, not with the application - this.win.webContents.setWindowOpenHandler(({ url }) => { - if (url.startsWith('https:')) { - this.log(`Opening external ${url} on browser`) - shell.openExternal(url) - } - return { action: 'deny' } - }) - - this.log('showing window') - this.win.show() - // win.webContents.on('will-navigate', (event, url) => { }) #344 - } -} diff --git a/lib/base.ts b/lib/base.ts deleted file mode 100644 index abd98dd6..00000000 --- a/lib/base.ts +++ /dev/null @@ -1,105 +0,0 @@ -/// - -import createDebug from 'debug' - -/** - * Base class for initializing KruxInstaller application, - * wdioTest runner and handlers. The `name` property will be used - * as identification for `debugger` object,as well a identification - * for ipcMain/ipcRenderer channel messages. - * - * @example - * ``` - * const base = new Base('krux:myClass') - * ``` - * - * @see WdioTest - * @see Storage - * @see Handler - */ -export default class Base { - /** - * Identificator to use as a attributed debugger and ipcMain/ipcRenderer channel messages - */ - protected name: KruxInstaller.DebugName; - - /** - * The attributed debuger - * - * @see debug.Debugger - */ - protected debug: debug.Debugger; - - /** - * The attributed electron browser window - * - * @see Electron.BrowserWindow - */ - protected win?: Electron.BrowserWindow; - - /** - * The attributed electron application - * - * @see Electron.App - */ - protected app?: Electron.App; - - /** - * If the environment variable DEBUG is attributed to 'background', any of the messages will be displayed only for this module; can use wildcards. - * - * @param name - * @example - * ``` - * $> DEBUG=background yarn run electron: - * $> DEBUG=background:* yarn run electron: - * ``` - */ - constructor(name: KruxInstaller.DebugName) { - this.name = name; - this.debug = createDebug(name); - this.log(`Initializing ${name}`); - } - - /** - * Send a message to debugger and to client - * window logger channel - * - * @param msg - * @example - * ``` - * log('Hello World') - * log({ hello: 'World' }) - * ``` - */ - log(msg: string | KruxInstaller.JsonDict): void { - this.send(null, msg) - } - - /** - * Send a message to a specific channel - * - * @param channel - * @param msg - * @example - * ``` - * send('my:channel', 'Hello World') - * send('my:channel', { hello: 'World' }) - * ``` - */ - send(channel: string | null, msg: KruxInstaller.JsonValue | KruxInstaller.JsonDict | string[]): void { - const __date__ = (new Date()).toLocaleString() - let __msg__ = '' - if (typeof msg === 'object') { - __msg__ = `[ ${__date__} ] ${JSON.stringify(msg)}` - } else { - __msg__ = `[ ${__date__} ] ${msg}` - } - this.debug(__msg__) - if (this.win !== null && this.win !== undefined) { - this.win.webContents.send('main-process-message', __msg__) - if (channel) { - this.win.webContents.send(channel, msg) - } - } - } -} diff --git a/lib/change-page.ts b/lib/change-page.ts deleted file mode 100644 index 5914fb35..00000000 --- a/lib/change-page.ts +++ /dev/null @@ -1,47 +0,0 @@ -/// - -import Handler from './handler' -import ElectronStore from 'electron-store'; - -export default class ChangePageHandler extends Handler { - - constructor (win: Electron.BrowserWindow, storage: ElectronStore, ipcMain: Electron.IpcMain) { - super('krux:change:page', win, storage, ipcMain); - } - - /** - * Builds a `handle` method for `ipcMain` to be called - * with `invoke` method in `ipcRenderer`. - * - * @example - * ``` - * // verify latest release from - * // https://api.github.com/repos/selfcustody/krux/git/refs/tags - * methods: { - * async download () { - * await window.api.invoke('krux:change:page') - * - * window.api.onSuccess('krux:change:page', function(_, list) { - * // ... do something - * }) - * - * window.api.onError('krux:change:page', function(_, error) { - * // ... do something - * }) - * } - * } - * - * ``` - */ - build () { - super.build(async (options: { page?: string, data?: any }) => { - try { - this.send(`${this.name}:success`, options) - } catch (error) { - this.log('Failed') - this.log(error) - this.send(`${this.name}:error`, { name: error.name, message: error.message, stack: error.stack.split('\n') }) - } - }) - } -} diff --git a/lib/check-if-it-will-flash.ts b/lib/check-if-it-will-flash.ts deleted file mode 100644 index c41e7ef0..00000000 --- a/lib/check-if-it-will-flash.ts +++ /dev/null @@ -1,91 +0,0 @@ -/// - -import ElectronStore from 'electron-store'; -import Handler from './handler' -import { join } from 'path'; -import { existsAsync } from './utils'; - -export default class CheckIfItWillFlashHandler extends Handler { - - constructor (win: Electron.BrowserWindow, storage: ElectronStore, ipcMain: Electron.IpcMain) { - super('krux:check:will:flash', win, storage, ipcMain); - } - - /** - * Builds a `handle` method for `ipcMain` to be called - * with `invoke` method in `ipcRenderer`. - * - * @example - * ``` - * // check if all requirements to flash - * // a firmware are meet (i.e. files for device) - * methods: { - * async download () { - * await window.api.invoke('krux:check:will:flash') - * - * window.api.onSuccess('krux:store:set', function(_, isChanged) { - * // ... do something - * }) - * - * window.api.onError('krux:check:will:flash', function(_, error) { - * // ... do something - * }) - * } - * } - * - * ``` - */ - build () { - super.build(async (options) => { - const device = this.storage.get('device') as string - const version = this.storage.get('version') as string - const resources = this.storage.get('resources') as string - const os = this.storage.get('os') as string - const isMac10 = this.storage.get('isMac10') as boolean - - if (device.match(/maixpy_(m5stickv|amigo|amigo_ips|amigo_tft|bit|dock|yahboom|cube)/g)) { - if (version.match(/selfcustody\/.*/g)) { - const __version__ = version.split('tag/')[1] - const destinationResourceZip = join(resources, __version__, `krux-${__version__}.zip`) - const destinationResourceSha = join(resources, __version__, `krux-${__version__}.zip.sha256.txt`) - const destinationResourceSig = join(resources, __version__, `krux-${__version__}.zip.sig`) - - if ( - await existsAsync(destinationResourceZip) && - await existsAsync(destinationResourceSha) && - await existsAsync(destinationResourceSig) - ) { - this.send(`${this.name}:success`, { showFlash: true }) - } else { - this.send(`${this.name}:success`, { showFlash: false }) - } - } else if (version.match(/odudex\/krux_binaries/g)) { - const destinationResourceFirmware = join(resources, version, 'main', device, 'firmware.bin') - const destinationResourceKboot = join(resources, version, 'main', device, 'kboot.kfpkg') - let destinationResourceKtool = '' - if (os === 'linux') { - destinationResourceKtool= join(resources, version, 'main', 'ktool-linux') - } else if (os === 'win32') { - destinationResourceKtool= join(resources, version, 'main', 'ktool-win.exe') - } else if (os === 'darwin' && !isMac10) { - destinationResourceKtool= join(resources, version, 'main', 'ktool-mac') - } else if (os === 'darwin' && isMac10) { - destinationResourceKtool= join(resources, version, 'main', 'ktool-mac-10') - } - - if ( - await existsAsync(destinationResourceFirmware) && - await existsAsync(destinationResourceKboot) && - await existsAsync(destinationResourceKtool) - ) { - this.send(`${this.name}:success`, { showFlash: true }) - } else { - this.send(`${this.name}:success`, { showFlash: false }) - } - } else { - this.send(`${this.name}:success`, { showFlash: false }) - } - } - }) - } -} diff --git a/lib/check-if-it-will-wipe.ts b/lib/check-if-it-will-wipe.ts deleted file mode 100644 index 5f3f43d8..00000000 --- a/lib/check-if-it-will-wipe.ts +++ /dev/null @@ -1,56 +0,0 @@ -/// - -import ElectronStore from 'electron-store'; -import Handler from './handler' -import { glob } from 'glob' - -export default class CheckIfItWillWipeHandler extends Handler { - - constructor (win: Electron.BrowserWindow, storage: ElectronStore, ipcMain: Electron.IpcMain) { - super('krux:check:will:wipe', win, storage, ipcMain); - } - - /** - * Builds a `handle` method for `ipcMain` to be called - * with `invoke` method in `ipcRenderer`. - * - * @example - * ``` - * // check if all requirements to flash - * // a firmware are meet (i.e. files for device) - * methods: { - * async download () { - * await window.api.invoke('krux:check:will:flash') - * - * window.api.onSuccess('krux:store:set', function(_, isChanged) { - * // ... do something - * }) - * - * window.api.onError('krux:check:will:flash', function(_, error) { - * // ... do something - * }) - * } - * } - * - * ``` - */ - build () { - super.build(async (options) => { - const device = this.storage.get('device') as string - const resources = this.storage.get('resources') as string - - if (device.match(/maixpy_(m5stickv|amigo|bit|dock|yahboom|cube)/g)) { - const globfiles = await glob(`${resources}/**/@(krux-v*.zip|ktool-*)`) - - if (globfiles.length > 0) { - this.send(`${this.name}:success`, { showWipe: true }) - } else { - console.log('no found') - this.send(`${this.name}:success`, { showWipe: false }) - } - } else { - this.send(`${this.name}:success`, { showWipe: false }) - } - }) - } -} diff --git a/lib/check-resource.ts b/lib/check-resource.ts deleted file mode 100644 index 0df29ce9..00000000 --- a/lib/check-resource.ts +++ /dev/null @@ -1,65 +0,0 @@ -/// - -import { join } from 'path' -import Handler from './handler' -import { existsAsync } from './utils' -import ElectronStore from 'electron-store'; - -export default class CheckResourcesHandler extends Handler { - - constructor (win: Electron.BrowserWindow, storage: ElectronStore, ipcMain: Electron.IpcMain) { - super('krux:check:resource', win, storage, ipcMain); - } - - /** - * Builds a `handle` method for `ipcMain` to be called - * with `invoke` method in `ipcRenderer`. - * - * @example - * ``` - * // check in client - * // if $HOME/documents/krux-installer/v22.08.2/krux-v22.08.2.zip - * // exists - * methods: { - * async check () { - * await window.api.invoke('krux:check:resource', 'v22.08.2/krux-v22.08.2.zip') - * - * // When the invoked method was successfully invoked, - * // it doesn't matter if it's true or false - * window.api.onSuccess('krux:check:resource', function(_, result) { - * // ... do something - * }) - * - * // When an error occurs - * window.api.onError('krux:check:resource', function(_, error) { - * // ... do something - * }) - * } - * } - * - * ``` - */ - build () { - super.build(async (options) => { - try { - const resources = this.storage.get('resources') as string - const destinationResource = join(resources, options.resource) - - this.log(`Checking if ${destinationResource} exists`) - - const __exists__ = await existsAsync(destinationResource) - - - this.send(`${this.name}:success`, { - from: options.from, - exists: __exists__, - baseUrl: options.baseUrl, - resourceFrom: options.resource, - resourceTo: destinationResource - }) - } catch (error) { - this.send(`${this.name}:error`, { name: error.name, message: error.message, stack: error.stack }) - } - }) - } -} \ No newline at end of file diff --git a/lib/download-resources.ts b/lib/download-resources.ts deleted file mode 100644 index c8150d36..00000000 --- a/lib/download-resources.ts +++ /dev/null @@ -1,147 +0,0 @@ -/// - -import { join, dirname } from 'path' -import { createWriteStream } from 'fs' -import axios, { AxiosRequestConfig } from 'axios' -import Handler from './handler' -import { existsAsync, mkdirAsync, rmAsync, formatBytes } from './utils' -import ElectronStore from 'electron-store' - -export default class DownloadResourcesHandler extends Handler { - - private resources: string; - private baseUrl: string; - private originResource: string; - private originFilename: string; - private destinationResource: string; - private destinationFilename: string; - - constructor (win: Electron.BrowserWindow, storage: ElectronStore, ipcMain: Electron.IpcMain) { - super('krux:download:resources', win, storage, ipcMain); - } - - /** - * Builds a `handle` method for `ipcMain` to be called - * with `invoke` method in `ipcRenderer`. - * - * @example - * ``` - * // Download in client - * // https://github.com/selfcustody/krux/releases/download/v22.08.2/krux-v22.08.2.zip - * // to $HOME/documents/krux-installer/v22.08.2/krux-v22.08.2.zip - * methods: { - * async download () { - * await window.api.invoke('krux:download:resources', { - * baseUrl: 'https://github.com/selfcustody/krux/releases/download', - * resource: 'v22.08.2', - * filename: 'krux-v22.08.2.zip' - * }) - * - * window.api.onData('krux:download:resources', function(_, percent) { - * // ... do something - * }) - * - * window.api.onSuccess('krux:download:resources', function(_, destinationFileName) { - * // ... do something - * }) - * - * window.api.onError('krux:download:resources', function(_, error) { - * // ... do something - * }) - * } - * } - * - * ``` - */ - build () { - super.build(async (options) => { - const { baseUrl, resourceFrom, resourceTo } = options - this.log(options) - - const destinationResource = dirname(resourceTo) - - try { - // First check if destination resource exists - // if not exists, then create a new directory - if (!await existsAsync(destinationResource)) { - this.log(`Creating directory ${destinationResource}`) - await mkdirAsync(destinationResource) - this.log(`Directory ${destinationResource} created`) - } - - // Check if destination filename exists - // if exists, remove it - if (await existsAsync(resourceTo)) { - this.log(`Removing existing ${resourceTo}`) - await rmAsync(resourceTo) - } - - // Setup download stream - const fullUrl = `${baseUrl}/${resourceFrom}` - this.log(`Downloading ${fullUrl}`) - - const file = createWriteStream(resourceTo) - const filename = resourceTo.split(`${destinationResource}/`)[1] - const axiosOpts = { - method: 'get', - url: fullUrl, - responseType: 'stream', - headers: { - 'Content-Disposition': `attachment filename=${filename}`, - 'User-Agent': `Chrome/${process.versions.chrome}`, - 'Connection': 'keep-alive', - 'Cache-Control': 'max-age=0', - 'Accept-Encoding': 'gzip, deflate, br' - } - } - this.log(axiosOpts) - const { data, headers } = await axios(axiosOpts as AxiosRequestConfig) - - // While download occurs - // use the chucks of data to show - // the current progress of download - let current = 0 - let percent: number | string = 0 - const totalLength = headers['content-length'] - percent = ((current/totalLength) * 100).toFixed(2) - this.log(`${resourceFrom} has ${formatBytes(totalLength)}`) - this.send(`${this.name}:data`, percent) - - data.on('data', (chunk) => { - current += chunk.length - percent = ((current/totalLength) * 100) - if (percent > 100.00) { - percent = 100.00 - } - percent = percent.toFixed(2) - this.send(`${this.name}:data`, percent) - }) - - data.on('finish', () => { - //file.close() - this.log(`${resourceFrom} downloaded`) - }) - - data.on('close', () => { - this.log(`Resource can be found in ${resourceTo}`) - - this.send(`${this.name}:success`, { - from: options.from, - resourceFrom: options.resourceFrom, - resourceTo: options.resourceTo - }) - }) - - data.on('error', (error) => { - this.log(error) - this.send(`${this.name}:error`, { name: error.name, message: error.message, stack: error.stack }) - }) - - data.pipe(file) - } catch (error) { - this.log(error) - this.send(`${this.name}:error`, { name: error.name, message: error.message, stack: error.stack }) - } - }) - } -} \ No newline at end of file diff --git a/lib/flash.ts b/lib/flash.ts deleted file mode 100644 index 9615de93..00000000 --- a/lib/flash.ts +++ /dev/null @@ -1,257 +0,0 @@ -/// - -import { spawn } from 'child_process' -import { join } from 'path' -import { SudoerLinux, SudoerDarwin } from '@o/electron-sudo/src/sudoer' -import ElectronStore from 'electron-store' -import Handler from './handler' -import { SerialPort } from 'serialport' - -export default class FlashHandler extends Handler { - - constructor (win: Electron.BrowserWindow, storage: ElectronStore, ipcMain: Electron.IpcMain) { - super('krux:flash', win, storage, ipcMain); - } - - /** - * Builds a `handle` method for `ipcMain` to be called - * with `invoke` method in `ipcRenderer`. - * - * @example - * ``` - * // change some key in store - * // some keys are forbidden to change - * // https://api.github.com/repos/selfcustody/krux/git/refs/tags - * methods: { - * async download () { - * await window.api.invoke('krux:store:set') - * - * window.api.onSuccess('krux:store:set', function(_, isChanged) { - * // ... do something - * }) - * - * window.api.onError('krux:store:set', function(_, error) { - * // ... do something - * }) - * } - * } - * - * ``` - */ - build () { - super.build(async (options) => { - // Store - const os = this.storage.get('os') as string - const isMac10 = this.storage.get('isMac10') as boolean - const resources = this.storage.get('resources') as string - const device = this.storage.get('device') as string - - // OS commands - const flash = { command: '', args: [] } - const chmod = { commands: [] } - - // dynamic variables - let version = this.storage.get('version') as string - let cwd = '' - - if (version.match(/selfcustody/g)) { - version = version.split('tag/')[1] - cwd = join(resources, `krux-${version}`) - } else if (version.match(/odudex/g)) { - version = join(version, 'main') - cwd = join(resources, version) - } - - // set correct kboot.kfpkg - const kboot = join(cwd, device, 'kboot.kfpkg') - - // set correct flash instructions - // if the device 'maixpy_dock' the board argument (-B) is 'dan', - // otherwise, is 'goE' - // SEE https://github.com/odudex/krux_binaries#flash-instructions - if (device.match(/maixpy_(m5stickv|amigo)/)) { - flash.args = ['--verbose', '-B', 'goE', '-b', '1500000', kboot] - try { - const ports = await SerialPort.list() - - // m5stickv and amigo has two ports - // get the first - let found = false - ports.forEach((port) => { - if (port.productId == "0403" && !found) { - this.send(`${this.name}:data`, `found device at ${port.path}\n`) - flash.args.push("-p") - flash.args.push(port.path) - found = true - } - }) - } catch (error) { - this.send(`${this.name}:error`, { was: 'flash', done: false, name: error.name, message: error.message, stack: error.stack }) - } - } else if (device.match(/maixpy_(bit|cube)/)) { - flash.args = ['--verbose', '-B', 'goE', '-b', '1500000', kboot] - try { - const ports = await SerialPort.list() - ports.forEach((port) => { - if (port.productId == "0403") { - this.send(`${this.name}:data`, `found device at ${port.path}\n`) - flash.args.push("-p") - flash.args.push(port.path) - } - }) - } catch (error) { - this.send(`${this.name}:error`, { was: 'flash', done: false, name: error.name, message: error.message, stack: error.stack }) - } - } else if (device.match(/maixpy_dock/g)) { - flash.args = ['--verbose', '-B', 'dan', '-b', '1500000', kboot] - try { - const ports = await SerialPort.list() - ports.forEach((port) => { - this.send(`${this.name}:data`, `found device at ${port.path}\n`) - if (port.productId == "7523") { - flash.args.push("-p") - flash.args.push(port.path) - } - }) - } catch (error) { - this.send(`${this.name}:error`, { was: 'flash', done: false, name: error.name, message: error.message, stack: error.stack }) - } - } else if (device.match(/maixpy_yahboom/g)){ - flash.args = ['--verbose', '-B', 'goE', '-b', '1500000', kboot] - try { - const ports = await SerialPort.list() - ports.forEach((port) => { - if (port.productId == "7523") { - this.send(`${this.name}:data`, `found device at ${port.path}\n`) - flash.args.push("-p") - flash.args.push(port.path) - } - }) - } catch (error) { - this.send(`${this.name}:error`, { was: 'flash', done: false, name: error.name, message: error.message, stack: error.stack }) - } - } else { - const error = new Error() - error.name = "Not Implemented Error" - error.message = `${device} isnt valid to flash` - this.send(`${this.name}:error`, { was: 'flash', done: false, name: error.name, message: error.message, stack: error.stack }) - } - - // Choose the correct ktool flasher - if (os === 'linux') { - flash.command = join(cwd, 'ktool-linux') - chmod.commands.push({ command: 'chmod', args: ['+x', flash.command] }) - } else if (os === 'win32') { - flash.command = join(cwd, 'ktool-win.exe') - } else if (os === 'darwin' && !isMac10) { - flash.command = join(cwd, 'ktool-mac') - chmod.commands.push({ command: 'chmod', args: ['+x', flash.command] }) - } else if (os === 'darwin' && isMac10) { - flash.command = join(cwd, 'ktool-mac-10') - chmod.commands.push({ command: 'chmod', args: ['+x', flash.command] }) - } - - // stack commands to be executed - const promises = chmod.commands.map((cmd) => { - return new Promise((resolve, reject) => { - let error = null - let buffer = Buffer.alloc(0) - - this.send(`${this.name}:data`, `\x1b[32m$> ${cmd.command} ${cmd.args.join(' ')}\x1b[0m\n\n`) - const script = spawn(cmd.command, cmd.args) - - script.stdout.on('data', (data) => { - buffer = Buffer.concat([buffer, data]) - this.send(`${this.name}:data`, buffer.toString()) - }) - - script.stderr.on('data', (data) => { - buffer = Buffer.concat([buffer, data]) - this.send(`${this.name}:data`, buffer.toString()) - error = true - }) - - script.on('close', (code) => { - if (error) { - error = new Error(buffer.toString()) - reject(error) - } - resolve() - }) - }) - }) - - await Promise.all(promises) - - // setup flash command - let flasher = null - - // special command for MacOS - let xattr = null - - this.send(`${this.name}:data`, `\x1b[32m$> ${flash.command} ${flash.args.join(' ')}\x1b[0m\n\n`) - - if (os === 'linux') { - const sudoer = new SudoerLinux() - flasher = await sudoer.spawn(flash.command, flash.args.join(' '), { env: process.env }) - } else if (os === 'darwin') { - const sudoer = new SudoerDarwin() - xattr = await sudoer.spawn('xattr', `-rc ${cwd}`, {env: process.env}) - flasher = await sudoer.spawn(flash.command, flash.args.join(' '), { env: process.env }) - } else if (os === 'win32') { - flasher = spawn(flash.command, flash.args) - } - - let err = null - let output = '' - - if (xattr !== null) { - xattr.stdout.on('data', (data: any) => { - output = Buffer.from(data, 'utf-8').toString() - this.send(`${this.name}:data`, output) - }) - - xattr.stderr.on('data', (data: any) => { - output = Buffer.from(data, 'utf-8').toString() - err = new Error(output) - this.send(`${this.name}:data`, output) - }) - - xattr.on('close', (code: any) => { - if (err) { - this.send(`${this.name}:error`, { was: 'flash', done: false , name: err.name, message: err.message, stack: err.stack }) - } - }) - } - - - flasher.stdout.on('data', (data: any) => { - output = Buffer.from(data, 'utf-8').toString() - if (output.match(/\[ERROR\].*/g)) { - output = output.replace("\x1b[31m", "") - output = output.replace("\x1b[1m", "") - output = output.replace("\x1b[0m", "") - output = output.replace("\x1b[32m", "") - output = output.replace("\x1b[0m \n", "") - output = output.replace("[ERROR]", "") - err = new Error(output) - } - this.send(`${this.name}:data`, output) - }) - - flasher.stderr.on('data', (data: any) => { - output = Buffer.from(data, 'utf-8').toString() - err = new Error(output) - this.send(`${this.name}:data`, output) - }) - - flasher.on('close', (code: any) => { - if (err) { - this.send(`${this.name}:error`, { was: 'flash', done: false , name: err.name, message: err.message, stack: err.stack }) - } else { - this.send(`${this.name}:success`, { was: 'flash', done: true }) - } - }) - }) - } -} diff --git a/lib/handler.ts b/lib/handler.ts deleted file mode 100644 index e70af5f8..00000000 --- a/lib/handler.ts +++ /dev/null @@ -1,29 +0,0 @@ -/// - -import ElectronStore from 'electron-store' -import Base from './base' - -/** - * Extended class to serve as 'base' for handlers; they will - * invoke ipcMain methods to comunicate with the BrowserWindow - */ -export default class Handler extends Base { - - protected storage: ElectronStore; - protected ipcMain: Electron.IpcMain; - - constructor(name: KruxInstaller.DebugName, win: Electron.BrowserWindow, storage: ElectronStore, ipcMain: Electron.IpcMain) { - super(name) - this.win = win - this.storage = storage - this.ipcMain = ipcMain - } - - build (callback: Function) { - this.log(`building ipcMain.handle for '${this.name}'`) - this.ipcMain.handle(`${this.name}`, async (_, options) => { - await callback(options) - }) - } - -} diff --git a/lib/quit.ts b/lib/quit.ts deleted file mode 100644 index f7e0577c..00000000 --- a/lib/quit.ts +++ /dev/null @@ -1,49 +0,0 @@ -/// - -import { app } from 'electron' -import ElectronStore from 'electron-store' -import Handler from './handler' - -export default class QuitHandler extends Handler { - - constructor (win: Electron.BrowserWindow, storage: ElectronStore, ipcMain: Electron.IpcMain) { - super('krux:quit', win, storage, ipcMain); - } - - /** - * Builds a `handle` method for `ipcMain` to be called - * with `invoke` method in `ipcRenderer`. - * - * @example - * ``` - * // change some key in store - * // some keys are forbidden to change - * // https://api.github.com/repos/selfcustody/krux/git/refs/tags - * methods: { - * async download () { - * await window.api.invoke('krux:quit') - * - * window.api.onSuccess('krux:quit', function(_, isChanged) { - * // ... do something - * }) - * - * window.api.onError('krux:quit', function(_, error) { - * // ... do something - * }) - * } - * } - * - * ``` - */ - build () { - super.build(async (options) => { - try { - this.send(`${this.name}:success`, '👋 Quiting krux-installer...') - app.quit() - process.exit(0) - } catch (error) { - this.send(`${this.name}:error`, { name: error.name, stack: error.stack, message: error.message }) - } - }) - } -} diff --git a/lib/storage.ts b/lib/storage.ts deleted file mode 100644 index 3810fb4e..00000000 --- a/lib/storage.ts +++ /dev/null @@ -1,154 +0,0 @@ -/// -/// - -import { join } from 'path' -import { spawn } from 'child_process' -import Base from './base' -import pkg from '../package.json' -import ElectronStore from 'electron-store' - -/* - * The KruxInstaller's persistent storage. - * If the environment variable DEBUG is attributed to 'krux:store' - * (DEBUG=krux:store yarn run electron:), - * any of the messages will be displayed - * only for this module - * @param app: The electron application - */ -export default class Storage extends Base { - - private config; - - constructor(app: Electron.App) { - super('krux:storage') - this.app = app - this.config = { - appVersion: { - type: 'string', - }, - resources: { - type: 'string', - }, - os: { - type: 'string' - }, - isMac10: { - type: 'boolean' - }, - versions: { - type: 'array', - }, - version: { - type: 'string', - regex: /(?:^odudex\/krux_binaries|selfcustody\/krux\/releases\/tag\/v\d+\.\d+\.\d+)/g - }, - device: { - type: 'string', - regex: /maixpy_(m5stickv5|amigo|dock|bit)/g - }, - showFlash: { - type: 'boolean' - } - } - } - - /** - * Verify, with `sw_vers` utility with `-productName` option - * the release type of the macOS, necessary to choose which - * ktool will be used (ktool-mac or ktool-mac-10) - */ - matchIsMac10 (): Promise { - // see - // https://www.shell-tips.com/mac/find-macos-version/#gsc.tab=0 - return new Promise((resolve, reject) => { - this.log('Checking macOS build') - const __cmd__ = 'sw_vers' - const __args__ = ['-productName'] - const __cwd__ = { cwd: '.' } - - this.log(`${__cmd__} ${__args__.join(" ")}` ) - const __sw_vers__ = spawn(__cmd__, __args__, __cwd__) - let isMac10 = false - - __sw_vers__.on('data', (data) => { - this.log(data) - if (data.match(/10.*/g)) { - isMac10 = true - } - }) - - __sw_vers__.on('err', (err) => { - this.log(err) - reject(new Error(err)) - }) - - __sw_vers__.on('close', (data) => { - this.log(`return code: ${data}`) - resolve(isMac10) - }) - }) - } - - /* - * Build folders: - * See https://www.electronjs.org/docs/latest/api/app#appgetpathname - * - * # appData - * - * - * - Linux: `$HOME/.config/krux-installer` - * - MacOS: `$HOME/Library/Application Support/krux-installer` - * - Windows: `$Env:APPDATA/krux-installer` - * - * # userData - * - * - Linux: `$HOME/Documents/krux-installer` - * - MacOS: `$HOME/Documents/krux-installer` - * - Windows: `$Env:MyDocuments/krux-installer` - * - */ - async build (): Promise { - try { - this.log('Starting storage') - const { default: Store } = await import('electron-store') - const store = new Store(this.config) - /* - * Variables to set store - * For mac, will be necessary to check, - * with `sw_vers` command, - * if the `productName` is something like - * `10.*.*`. This will be necessary when using - * `ktook-*` command (where * is `mac` or `mac-10`). - */ - const resourcePath = join(this.app.getPath('documents'), pkg.name) - const versions = [] - const version = 'Select version' - const device = 'Select device' - let isMac10 = false - const showFlash = false - - if (process.platform === 'darwin') { - isMac10 = await this.matchIsMac10() - } - - store.set('appVersion', pkg.version) - store.set('resources', resourcePath) - store.set('os', process.platform) - store.set('isMac10', isMac10) - store.set('versions', versions) - store.set('version', version) - store.set('device', device) - store.set('showFlash', showFlash) - - const keys = Object.keys(this.config) - for (let k in keys) { - const key = keys[k] - const value = store.get(key) - this.log(` ${key}: ${value}`) - } - return store - } catch (error) { - this.log(error) - } - } -} diff --git a/lib/store-get.ts b/lib/store-get.ts deleted file mode 100644 index 5ff56da1..00000000 --- a/lib/store-get.ts +++ /dev/null @@ -1,62 +0,0 @@ -/// - -import ElectronStore from 'electron-store'; -import Handler from './handler' - -export default class StoreGetHandler extends Handler { - - constructor (win: Electron.BrowserWindow, storage: ElectronStore, ipcMain: Electron.IpcMain) { - super('krux:store:get', win, storage, ipcMain); - } - - /** - * Builds a `handle` method for `ipcMain` to be called - * with `invoke` method in `ipcRenderer`. - * - * @example - * ``` - * // get some value from key in store - * // - "appVersion" - * // - "resources" - * // - "os" - * // - "isMac10" - * // - "versions" - * // - "version" - * // - "device" - * // - "sdcard" - - * methods: { - * async download () { - * // keys: - * await window.api.invoke('krux:store:get', { key: 'version' }) - * - * window.api.onSuccess('krux:store:get', function(_, value) { - * // ... do something - * }) - * - * window.api.onError('krux:store:get', function(_, error) { - * // ... do something - * }) - * } - * } - * - * ``` - */ - build() { - super.build((options: { from: string, keys: string[] }) => { - try { - const result = { - from: options.from, - values: {} - } - for (let i in options.keys) { - const key = options.keys[i] - result.values[key] = this.storage.get(key) as KruxInstaller.JsonValue - } - this.send(`${this.name}:success`, result) - } catch (error) { - this.send(`${this.name}:error`, error) - } - }) - } -} \ No newline at end of file diff --git a/lib/store-set.ts b/lib/store-set.ts deleted file mode 100644 index b05502f0..00000000 --- a/lib/store-set.ts +++ /dev/null @@ -1,65 +0,0 @@ -/// - -import ElectronStore from 'electron-store'; -import Handler from './handler' - -export default class StoreSetHandler extends Handler { - - constructor (win: Electron.BrowserWindow, storage: ElectronStore, ipcMain: Electron.IpcMain) { - super('krux:store:set', win, storage, ipcMain); - } - - /** - * Builds a `handle` method for `ipcMain` to be called - * with `invoke` method in `ipcRenderer`. - * - * @example - * ``` - * // change some key in store - * // some keys are forbidden to change - * // https://api.github.com/repos/selfcustody/krux/git/refs/tags - * methods: { - * async download () { - * await window.api.invoke('krux:store:set') - * - * window.api.onSuccess('krux:store:set', function(_, isChanged) { - * // ... do something - * }) - * - * window.api.onError('krux:store:set', function(_, error) { - * // ... do something - * }) - * } - * } - * - * ``` - */ - build () { - super.build((options) => { - if ( - options.key !== 'appVersion' || - options.key !== 'resources' || - options.key !== 'os' || - options.key !== 'isMac10' || - options.key !== 'versions' || - options.key !== 'version' - ) { - this.storage.set(options.key, options.value) - const newValue = this.storage.get(options.key) - const result = { - ...options, - value: newValue as string - } - - // Little hack to set 'showFlash' - // if - - this.send(`${this.name}:success`, result) - } else { - const error = Error(`Forbidden: cannot set '${options.key}'`) - this.log(error.stack) - this.send(`${this.name}:error`, error.stack) - } - }) - } -} \ No newline at end of file diff --git a/lib/unzip-resource.ts b/lib/unzip-resource.ts deleted file mode 100644 index 2a06be5b..00000000 --- a/lib/unzip-resource.ts +++ /dev/null @@ -1,204 +0,0 @@ -/// - -import { join } from 'path' -import { createWriteStream } from 'fs' -import { ZipFile, open } from 'yauzl' -import { glob } from 'glob' -import { mkdirAsync } from './utils' -import Handler from './handler' -import ElectronStore from 'electron-store' - -function openZipFile (filepath: string): Promise { - return new Promise(function (resolve, reject) { - open(filepath, { lazyEntries: true }, function (err, zipfile) { - if (err) { - reject(err) - } else { - resolve(zipfile) - } - }) - }) -} - -export default class UnzipResourceHandler extends Handler { - - constructor (win: Electron.BrowserWindow, storage: ElectronStore, ipcMain: Electron.IpcMain) { - super('krux:unzip', win, storage, ipcMain) - } - - async onUnzip (zipFilePath: string, resources: string, device: string, os: string, isMac10: boolean, options: { will?: any }) { - this.send(`${this.name}:data`, `Extracting ${zipFilePath}

`) - - const zipfile = await openZipFile(zipFilePath) - zipfile.readEntry() - - // Each fileName should be added to entries array - // that will be returned to client application - // This event should extract each file to - // a destination folder defined in store - zipfile.on('entry', async (entry) => { - - // Directory file names end with '/'. - // Note that entries for directories themselves are optional. - // An entry's fileName implicitly requires its parent directories to exist. - const destination = join(resources, entry.fileName) - - if (/\/$/.test(entry.fileName)) { - const onlyRootKruxFolder = /^(.*\/)?krux-v[0-9\.]+\/$/ - const deviceKruxFolder = new RegExp(`^(.*\/)?krux-v[0-9\.]+\/${device}\/$`) - if (onlyRootKruxFolder.test(entry.fileName) || deviceKruxFolder.test(entry.fileName)) { - this.send(`${this.name}:data`, `Creating ${destination}

`) - await mkdirAsync(destination) - } - zipfile.readEntry(); - } else { - let ktoolKrux: RegExp; - let deviceKruxFirmwareBin: RegExp; - let deviceKruxFirmwareBinSig: RegExp; - let deviceKruxKboot: RegExp; - - if (os === 'linux') { - ktoolKrux = /^(.*\/)?krux-v[0-9\.]+\/ktool-linux$/ - } else if (os === 'darwin' && !isMac10) { - ktoolKrux = /^(.*\/)?krux-v[0-9\.]+\/ktool-mac$/ - } else if (os === 'darwin' && isMac10) { - ktoolKrux = /^(.*\/)?krux-v[0-9\.]+\/ktool-mac-10$/ - } else if (os === 'win32') { - ktoolKrux = /^(.*\\)?krux-v[0-9\.]+\\ktool-win\.exe$/ - } - - if (os === 'linux' || os === 'darwin') { - deviceKruxFirmwareBin = new RegExp(`^(.*\/)?krux-v[0-9\.]+\/${device}\/firmware.bin$`) - deviceKruxFirmwareBinSig = new RegExp(`^(.*\/)?krux-v[0-9\.]+\/${device}\/firmware.bin.sig$`) - deviceKruxKboot = new RegExp(`^(.*\/)?krux-v[0-9\.]+\/${device}\/kboot.kfpkg$`) - } else if (os === 'win32') { - deviceKruxFirmwareBin = new RegExp(`^(.*\\\\)?krux-v[0-9\.]+\\\\${device}\\\\firmware.bin$`) - deviceKruxFirmwareBinSig = new RegExp(`^(.*\\\\)?krux-v[0-9\.]+\\\\${device}\\\\firmware.bin.sig$`) - deviceKruxKboot = new RegExp(`^(.*\\\\)?krux-v[0-9\.]+\\\\${device}\\\\kboot.kfpkg$`) - } - - // (only extract device related files) - if ( - deviceKruxFirmwareBin.test(destination) || - deviceKruxFirmwareBinSig.test(destination) || - deviceKruxKboot.test(destination) || - ktoolKrux.test(destination) - ) { - // create the destination file - const writeStream = createWriteStream(destination) - - this.send(`${this.name}:data`, `Extracting ${entry.fileName}...

`) - - // extract it - zipfile.openReadStream(entry, (entryError, readStream) => { - if (entryError) { - this.send(`${this.name}:error`, { name: entryError.name, message: entryError.message, stack: entryError.stack }) - } else { - readStream.on('end', () => { - this.send(`${this.name}:data`, `Extracted to ${destination}

`) - zipfile.readEntry() - }) - - readStream.on('error', (streamErr) => { - this.send(`${this.name}:error`, { name: streamErr.name, message: streamErr.message, stack: streamErr.stack }) - }) - - readStream.pipe(writeStream) - } - }) - } else { - zipfile.readEntry() - } - } - }) - - zipfile.on('end', () => { - zipfile.close() - this.send(`${this.name}:success`, { will: options.will }) - }) - - zipfile.on('error', (zipErr) => { - this.send(`${this.name}:error`, { name: zipErr.name, message: zipErr.message, stack: zipErr.stack }) - }) - } - - /** - * Builds a `handle` method for `ipcMain` to be called - * with `invoke` method in `ipcRenderer`. - * - * @example - * ``` - * // unzip in client - * // the $HOME/{{ Documents folder }}/krux-installer/{{ latest-version }}/krux-{{ latest-version }}.zip resource - * methods: { - * async check () { - * await window.api.invoke('krux:unzip') - * - // When the invoked method was successfully invoked, - * // it doesn't matter if it's true or false - * window.api.onData('krux:unzip', function(_, message) { - * // ... do something - * }) - - * // When the invoked method was successfully invoked, - * // it doesn't matter if it's true or false - * window.api.onSuccess('krux:unzip', function() { - * // ... do something - * }) - * - * // When an error occurs - * window.api.onError('krux:unzip', function(_, error) { - * // ... do something - * }) - * } - * } - * - * ``` - */ - build () { - super.build(async (options: { will?: any }) => { - try { - // Only unzip if is a selfcustody version - let version = this.storage.get('version') as string; - const device = this.storage.get('device') as string; - const resources = this.storage.get('resources') as string; - const os = this.storage.get('os') as string; - const isMac10 = this.storage.get('isMac10') as boolean; - - if (version.match(/selfcustody.*/g)) { - version = version.split('tag/')[1]; - const zipFilePath = join(resources, version, `krux-${version}.zip`) - - if (version.match(/24\.\d+\.\d+/)) { - if (device.match(/maixpy_amigo_(tft|ips)/g)) { - const error = new Error(`Device '${device}' not used anymore in version ${version}. Use 'maixpy_amigo' instead`) - this.send(`${this.name}:error`, { name: error.name, message: error.message, stack: error.stack}) - } - //if (device.match(/maixpy_cube/g)) { - // const error = new Error(`Device '${device}' not implemented for version '${version}'`) - // this.send(`${this.name}:error`, { name: error.name, message: error.message, stack: error.stack}) - //} - } - this.onUnzip(zipFilePath, resources, device, os, isMac10, options) - } else if (version === 'Select version') { - const globfiles = await glob(`${resources}/**/@(krux-v*.zip|ktool-*)`) - if (globfiles.length > 0) { - if (globfiles[0].includes('.zip')) { - const zipFilePath = globfiles[0] - this.onUnzip(zipFilePath, resources, device, os, isMac10, options) - } else { - this.send(`${this.name}:success`, { will: options.will }) - } - } else { - const error = new Error("No ktool found") - this.send(`${this.name}:error`, { name: error.name, message: error.message, stack: error.stack}) - } - } else { - this.send(`${this.name}:success`, { will: options.will }) - } - } catch (error) { - this.send(`${this.name}:error`, { name: error.name, message: error.message, stack: error.stack }) - } - }) - } -} diff --git a/lib/utils.ts b/lib/utils.ts deleted file mode 100644 index 1a9ba3d7..00000000 --- a/lib/utils.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { copyFile, readFile, mkdir, exists, unlink } from 'fs' -import { promisify } from 'util' - -/** - * Copy file in asynchronous manner - * - * @param origin: the full path of origin file - * @param destination: the full path of destination file - */ -export const copyFileAsync = promisify(copyFile) - -/** - * Copy file in asynchronous manner - * - * @param origin: the full path of origin file - * @param destination: the full path of destination file - */ -export const readFileAsync = promisify(readFile) - -/* - * Function to check if file or folder exists - * in async/await approach. Always resoulves to - * a boolean value. - * - * @param p: path of the file - * @return Boolean - */ -export const existsAsync = promisify(exists) - -/* - * Function to check if file or folder exists - * in async/await approach. Always resoulves to - * a boolean value. - * - * @param p: path of the file - * @return Boolean - */ -export const rmAsync = promisify(unlink) - -/* - * Function to create folder recursively - * in async/await approach. Throws - * an error if any occurs. - * - * @param p: path of the file - * @throw Error: if some error occurs - */ -export function mkdirAsync(p: string): Promise { - return new Promise((resolve, reject) => { - mkdir(p, { recursive: true }, function(err) { - if (err) reject(err) - resolve() - }) - }) -} - - -/* - * Function to format the size in bytes - * - * found at https://stackoverflow.com/questions/15900485/correct-way-to-convert-size-in-bytes-to-kb-mb-gb-in-javascript#18650828 - */ -export function formatBytes(bytes, decimals: number = 2): string { - if (bytes === 0) return '0 Bytes' - const k = 1024 - const dm = decimals < 0 ? 0 : decimals - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] - const i = Math.floor(Math.log(bytes) / Math.log(k)) - return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] -} diff --git a/lib/verify-official-releases-fetch.ts b/lib/verify-official-releases-fetch.ts deleted file mode 100644 index c9092d65..00000000 --- a/lib/verify-official-releases-fetch.ts +++ /dev/null @@ -1,74 +0,0 @@ -/// - -import axios from 'axios' -import Handler from './handler' -import ElectronStore from 'electron-store'; - -export default class VerifyOfficialReleasesFetchHandler extends Handler { - - constructor (win: Electron.BrowserWindow, storage: ElectronStore, ipcMain: Electron.IpcMain) { - super('krux:verify:releases:fetch', win, storage, ipcMain); - } - - /** - * Builds a `handle` method for `ipcMain` to be called - * with `invoke` method in `ipcRenderer`. - * - * @example - * ``` - * // verify latest release from - * // https://api.github.com/repos/selfcustody/krux/git/refs/tags - * methods: { - * async download () { - * await window.api.invoke('krux:verify:releases:fetch') - * - * window.api.onSuccess('krux:verify:releases:fetch', function(_, list) { - * // ... do something - * }) - * - * window.api.onError('krux:verify:releases:fetch', function(_, error) { - * // ... do something - * }) - * } - * } - * - * ``` - */ - build () { - super.build(async (options) => { - const url = 'https://api.github.com/repos/selfcustody/krux/git/refs/tags' - let releases: string[] = [] - try { - this.log(`Fetching ${url}`) - const response = await axios({ - method: 'get', - url: url, - headers: { - 'User-Agent': `Chrome/${process.versions.chrome}` - } - }) - if (response.status === 200) { - this.log(response.data) - } else { - throw new Error(`${url} returned ${response.status} status code`) - } - - this.log('Saving fetched versions in storage') - const list = [] - - // verify for newest release - const version = response.data[response.data.length - 1]["ref"].split('tags/')[1] as string - list.push(`selfcustody/krux/releases/tag/${version}`) - list.push('odudex/krux_binaries') - this.storage.set('versions', list) - const result = { - from: options.from, - value: this.storage.get('versions') - } - this.send(`${this.name}:success`, result as KruxInstaller.JsonDict) - } catch (error) { - this.send(`${this.name}:error`, error) - } - }) - } -} \ No newline at end of file diff --git a/lib/verify-official-releases-hash.ts b/lib/verify-official-releases-hash.ts deleted file mode 100644 index 72846a5c..00000000 --- a/lib/verify-official-releases-hash.ts +++ /dev/null @@ -1,110 +0,0 @@ -/// - -import { join } from 'path' -import { createHash } from 'crypto' -import { readFileAsync, existsAsync } from './utils' -import Handler from './handler' -import ElectronStore from 'electron-store'; - -export default class VerifyOfficialReleasesHashHandler extends Handler { - - constructor (win: Electron.BrowserWindow, storage: ElectronStore, ipcMain: Electron.IpcMain) { - super('krux:verify:releases:hash', win, storage, ipcMain); - } - - /** - * Builds a `handle` method for `ipcMain` to be called - * with `invoke` method in `ipcRenderer`. - * - * @example - * ``` - * // verify latest release from - * // https://api.github.com/repos/selfcustody/krux/git/refs/tags - * methods: { - * async download () { - * await window.api.invoke('krux:verify:releases:hash', - * 'v22.08.2/krux-v22.08.2.zip', - * 'v22.08.2/krux-v22.08.2.zip.sha256.txt' - * ) - * - * window.api.onSuccess('krux:verify:releases:hash', function(_, list) { - * // ... do something - * }) - * - * window.api.onError('krux:verify:releases:hash', function(_, error) { - * // ... do something - * }) - * } - * } - * - * ``` - */ - build () { - super.build((_: Event, options: Record )=> { - const result = [] - const resources = this.storage.get('resources')as string - const version = this.storage.get('version') as string - const resource = version.split('tag/')[1] - - const zipFileRelPath = `${resource}/krux-${resource}.zip` - const shaFileRelPath = `${resource}/krux-${resource}.zip.sha256.txt` - - const zipFilePath = join(resources, zipFileRelPath) - const shaFilePath = join(resources, shaFileRelPath) - - // Maybe the sha256txt file could be downloade - // after checking, so we will check if the file exists - // and the string represenation is valid - // every second, and then, return the result to client - const verify = async (p) => { - try { - const exists = await existsAsync(p) - const sha256buffer = await readFileAsync(shaFilePath, null) - const sha256txt = sha256buffer.toString().split(" ")[0] - - if (exists && sha256txt !== '') { - result.push({ - name: shaFileRelPath, - value: sha256txt.replace(/[\n\t\r]/g,'') - }) - - const zipBuffer = await readFileAsync(zipFilePath, null) - const hashSum = createHash('sha256') - hashSum.update(zipBuffer) - - result.push({ - name: zipFileRelPath, - value: hashSum.digest('hex') - }) - - const isMatch = result[0].value === result[1].value - - if (isMatch) { - const msg = [ - 'sha256sum match:', - `${result[0].name} has a ${result[1].value} hash`, - `and ${result[1].name} summed a hash ${result[1].value}.` - ].join(' ') - this.log(msg) - this.send(`${this.name}:success`, result) - } else { - const msg = [ - 'sha256sum match error:', - `${result[0].name} has a hash of ${result[0].value}`, - `and ${result[1].name} summed a hash of ${result[1].value}` - ].join(' ') - const error = new Error(msg) - this.send(`${this.name}:error`, { name: error.name, message: error.message, stack: error.stack }) - } - clearInterval(interval) - } - } catch (error) { - this.log(error) - this.send(`${this.name}:error`, error) - } - } - this.log(`Verifying ${zipFilePath} against ${shaFilePath}`) - const interval = setInterval(verify, 1000, shaFilePath) - }) - } -} diff --git a/lib/verify-official-releases-sign.ts b/lib/verify-official-releases-sign.ts deleted file mode 100644 index 28034710..00000000 --- a/lib/verify-official-releases-sign.ts +++ /dev/null @@ -1,119 +0,0 @@ -/// - -import { join } from 'path' -import { spawn } from 'child_process' -import Handler from './handler' -import ElectronStore from 'electron-store'; - -export default class VerifyOfficialReleasesSignHandler extends Handler { - - constructor (win: Electron.BrowserWindow, storage: ElectronStore, ipcMain: Electron.IpcMain) { - super('krux:verify:releases:sign', win, storage, ipcMain); - } - - /** - * Builds a `handle` method for `ipcMain` to be called - * with `invoke` method in `ipcRenderer`. - * - * @example - * ``` - * // verify the signature of official releases - * methods: { - * async download () { - * await window.api.invoke('krux:verify:releases:sig', { - * bin: 'v22.08.2/krux-v22.08.2.zip', - * pem: 'main/selfcustody.pem', - * sig: 'v22.08.2/krux-v22.08.2.zip.sig' - * }) - * - * window.api.onSuccess('krux:verify:releases:sig', function(_, result) { - * // ... do something - * }) - * - * window.api.onError('krux:verify:releases:hash', function(_, error) { - * // ... do something - * }) - * } - * } - * - * ``` - */ - build (): void { - super.build((_, options) => { - const resources = this.storage.get('resources') as string - const version = this.storage.get('version') as string - - const zipResource = version.split('tag/')[1] - const zipFileRelPath = `${zipResource}/krux-${zipResource}.zip` - const sigFileRelPath = `${zipResource}/krux-${zipResource}.zip.sig` - const pemFileRelPath = 'main/selfcustody.pem' - - - const binPath = join(resources, zipFileRelPath) - const pemPath = join(resources, pemFileRelPath) - const sigPath = join(resources, sigFileRelPath) - const platform = this.storage.get('os') - - // if platform is linux, use the same command - // used in krux CLI. Else use sign-check package - let shell = '' - let compileArg = '' - let opensslBin = '' - - if (platform === 'linux') { - shell = "/bin/bash" - compileArg = "-c" - opensslBin = "openssl" - } else if (platform === 'darwin') { - shell = "/bin/zsh" - compileArg = "-c" - opensslBin = "openssl" - } else { - shell = "cmd" - compileArg = "/c" - opensslBin = "openssl.exe" - } - - // you can still omit the quotes - // and everything will execute correctly - // through openssl, - // since child_process will pass it as a single argument: - // See: - // https://stackoverflow.com/questions/27670686/ssh-with-nodejs-child-process-command-not-found-on-server - const signCmd = `${opensslBin} sha256 <${binPath} -binary | ${opensslBin} pkeyutl -verify -pubin -inkey ${pemPath} -sigfile ${sigPath}` - this.log(`${shell} ${compileArg} ${signCmd}`) - - let stdout = Buffer.alloc(0) - let isErr = false - const openssl = spawn(shell, [compileArg, signCmd]) - - openssl.stdout.on('data', (chunk) => { - this.log(`stdout: ${chunk}`) - stdout = Buffer.concat([stdout, chunk]) - }) - - openssl.stderr.on('data', (chunk) => { - this.log(`stderr: ${chunk}`) - stdout = Buffer.concat([stdout, chunk]) - isErr = true - }) - - openssl.on('error', (err) => { - this.send(`${this.name}:error`, { name: err.name, message: err.message, stack: err.stack }) - }) - - openssl.on('close', (code) => { - this.log(`${opensslBin} exited with code ${code}`) - if (isErr) { - const err = new Error(stdout.toString()) - this.send(`${this.name}:error`, { name: err.name, message: err.message, stack: err.stack }) - } else { - this.send(`${this.name}:success`, { - command: signCmd, - sign: stdout.toString() - }) - } - }) - }) - } -} \ No newline at end of file diff --git a/lib/verify-openssl.ts b/lib/verify-openssl.ts deleted file mode 100644 index 77b429d2..00000000 --- a/lib/verify-openssl.ts +++ /dev/null @@ -1,63 +0,0 @@ -/// - -import ElectronStore from 'electron-store'; -import Handler from './handler' -import commandExists from 'command-exists' - -export default class VerifyOpensslHandler extends Handler { - - constructor (win: Electron.BrowserWindow, storage: ElectronStore, ipcMain: Electron.IpcMain) { - super('krux:verify:openssl', win, storage, ipcMain); - } - - /** - * Builds a `handle` method for `ipcMain` to be called - * with `invoke` method in `ipcRenderer`. - * - * @example - * ``` - * methods: { - * async verify () { - * // keys: - * await window.api.invoke('krux:verify:openssl', 'version') - * - * window.api.onSuccess('krux:verify:openssl', function(_, value) { - * // ... do something - * }) - * - * window.api.onError('krux:verify:openssl', function(_, error) { - * // ... do something - * }) - * } - * } - * - * ``` - */ - build (): void { - super.build(async (options: { from: string }) => { - try { - const platform = this.storage.get('os') - let msg = '' - let exists: string = '' - if (platform === 'linux' || platform === 'darwin' ) { - exists = await commandExists('openssl') - } - else if (platform === 'win32') { - exists = await commandExists('openssl.exe') - } else { - throw new Error(`neither "openssl" or "openssl.exe" found for ${platform}`) - } - - msg = "openssl for "+platform+`${exists ? " found" : "not found"}` - - const result = { - ...options, - message: msg - } - this.send(`${this.name}:success`, result) - } catch (error) { - this.send(`${this.name}:error`, error) - } - }) - } -} \ No newline at end of file diff --git a/lib/wdio-test.ts b/lib/wdio-test.ts deleted file mode 100644 index 748862b1..00000000 --- a/lib/wdio-test.ts +++ /dev/null @@ -1,37 +0,0 @@ -/// -/// -/// - -import Base from './base' -import electron, { BrowserWindow, dialog } from 'electron'; -/** - * Extend `Base` class for initializing KruxInstaller storage, - * assigning `krux:wdio:e2e` to `name` property. - * - * Since electron 20 - * the approach descibed in - * https://webdriver.io/docs/wdio-electron-service/#example-configuration - * do not works. - * The 'solution' was copy the contents from node-modules/wdio-electron-service/dist/main - * and reconfigure some variables - * - * @see Electron.IpcMain - */ -export default class WdioTestHandler extends Base { - - protected app: Electron.App; - protected ipcMain: Electron.IpcMain; - - constructor (app: Electron.App, ipcMain: Electron.IpcMain) { - super('krux:wdio:e2e'); - this.app = app - this.ipcMain = ipcMain - } - - /* - * Create `wdio-electron` and `wdio-electron.app` ipcMain handlers` - */ - build (): void { - - } -} \ No newline at end of file diff --git a/lib/wipe.ts b/lib/wipe.ts deleted file mode 100644 index 88f1baf3..00000000 --- a/lib/wipe.ts +++ /dev/null @@ -1,222 +0,0 @@ -/// - -import { spawn } from 'child_process' -import { join } from 'path' -import { SudoerLinux, SudoerDarwin } from '@o/electron-sudo/src/sudoer' -import ElectronStore from 'electron-store' -import Handler from './handler' -import { SerialPort } from 'serialport' -import { glob } from 'glob' - -export default class FlashHandler extends Handler { - - constructor (win: Electron.BrowserWindow, storage: ElectronStore, ipcMain: Electron.IpcMain) { - super('krux:wipe', win, storage, ipcMain); - } - - /** - * Builds a `handle` method for `ipcMain` to be called - * with `invoke` method in `ipcRenderer`. - * - * @example - * ``` - * // change some key in store - * // some keys are forbidden to change - * // https://api.github.com/repos/selfcustody/krux/git/refs/tags - * methods: { - * async download () { - * await window.api.invoke('krux:store:set') - * - * window.api.onSuccess('krux:store:set', function(_, isChanged) { - * // ... do something - * }) - * - * window.api.onError('krux:store:set', function(_, error) { - * // ... do something - * }) - * } - * } - * - * ``` - */ - build () { - super.build(async (options) => { - // Store - const os = this.storage.get('os') as string - const isMac10 = this.storage.get('isMac10') as boolean - const resources = this.storage.get('resources') as string - const device = this.storage.get('device') as string - - // OS commands - const flash = { command: '', args: [] } - const chmod = { commands: [] } - - // set correct flash instructions - // if the device 'maixpy_dock' the board argument (-B) is 'dan', - // otherwise, is 'goE' - // SEE https://github.com/odudex/krux_binaries#flash-instructions - if (device.match(/maixpy_(m5stickv|amigo)/)) { - flash.args = ['--verbose', '-B', 'goE', '-b', '1500000'] - try { - const ports = await SerialPort.list() - - // m5stickv and amigo has two ports - // get the first - let found = false - ports.forEach((port) => { - if (port.productId == "0403" && !found) { - flash.args.push("-p") - flash.args.push(port.path) - found = true - } - }) - } catch (err) { - this.send(`${this.name}:error`, { was: 'wipe', done: false , name: err.name, message: err.message, stack: err.stack }) - } - } else if (device.match(/maixpy_(bit|cube)/)) { - flash.args = ['--verbose', '-B', 'goE', '-b', '1500000'] - try { - const ports = await SerialPort.list() - ports.forEach((port) => { - if (port.productId == "0403") { - flash.args.push("-p") - flash.args.push(port.path) - } - }) - } catch (err) { - this.send(`${this.name}:error`, { was: 'wipe', done: false , name: err.name, message: err.message, stack: err.stack }) - } - } else if (device.match(/maixpy_dock/g)) { - flash.args = ['--verbose', '-B', 'dan', '-b', '1500000'] - try { - const ports = await SerialPort.list() - ports.forEach((port) => { - if (port.productId == "7523") { - flash.args.push("-p") - flash.args.push(port.path) - } - }) - } catch (err) { - this.send(`${this.name}:error`, { was: 'wipe', done: false , name: err.name, message: err.message, stack: err.stack }) - } - } else if (device.match(/maixpy_yahboom/g)){ - flash.args = ['--verbose', '-B', 'goE', '-b', '1500000'] - try { - const ports = await SerialPort.list() - ports.forEach((port) => { - if (port.productId == "7523") { - flash.args.push("-p") - flash.args.push(port.path) - } - }) - } catch (error) { - this.send(`${this.name}:error`, { was: 'wipe', done: false, name: error.name, message: error.message, stack: error.stack }) - } - } else { - const error = new Error() - error.name = "Not Implemented Error" - error.message = `${device} isnt valid to flash` - this.send(`${this.name}:error`, { was: 'wipe', done: false, name: error.name, message: error.message, stack: error.stack }) - } - - flash.args.push('-E') - - // Choose the correct ktool flasher - let globfiles: string[] - if (os === 'linux') { - globfiles = await glob(`${resources}/**/ktool-linux`) - flash.command = globfiles[0] - chmod.commands.push({ command: 'chmod', args: ['+x', flash.command] }) - } else if (os === 'win32') { - globfiles = await glob(`${resources}/**/ktool-win.exe`) - flash.command = globfiles[0] - } else if (os === 'darwin' && !isMac10) { - globfiles = await glob(`${resources}/**/ktool-mac`) - flash.command = globfiles[0] - chmod.commands.push({ command: 'chmod', args: ['+x', flash.command] }) - } else if (os === 'darwin' && isMac10) { - globfiles = await glob(`${resources}/**/ktool-mac-10`) - flash.command = globfiles[0] - chmod.commands.push({ command: 'chmod', args: ['+x', flash.command] }) - } - - // stack commands to be executed - const promises = chmod.commands.map((cmd) => { - return new Promise((resolve, reject) => { - let error = null - let buffer = Buffer.alloc(0) - - this.send(`${this.name}:data`, `\x1b[32m$> ${cmd.command} ${cmd.args.join(' ')}\x1b[0m\n\n`) - const script = spawn(cmd.command, cmd.args) - - script.stdout.on('data', (data) => { - buffer = Buffer.concat([buffer, data]) - this.send(`${this.name}:data`, buffer.toString()) - }) - - script.stderr.on('data', (data) => { - buffer = Buffer.concat([buffer, data]) - this.send(`${this.name}:data`, buffer.toString()) - error = true - }) - - script.on('close', (code) => { - if (error) { - error = new Error(buffer.toString()) - reject(error) - } - resolve() - }) - }) - }) - - await Promise.all(promises) - - // setup flash command - let flasher = null - - this.send(`${this.name}:data`, `\x1b[32m$> ${flash.command} ${flash.args.join(' ')}\x1b[0m\n\n`) - - if (os === 'linux') { - const sudoer = new SudoerLinux() - flasher = await sudoer.spawn(flash.command, flash.args.join(' '), { env: process.env }) - } else if (os === 'darwin') { - const sudoer = new SudoerDarwin() - flasher = await sudoer.spawn(flash.command, flash.args.join(' '), { env: process.env }) - } else if (os === 'win32') { - flasher = spawn(flash.command, flash.args) - } - - let err = null - let output = '' - - flasher.stdout.on('data', (data: any) => { - output = Buffer.from(data, 'utf-8').toString() - if (output.match(/\[ERROR\].*/g)) { - output = output.replace("\x1b[31m", "") - output = output.replace("\x1b[1m", "") - output = output.replace("\x1b[0m", "") - output = output.replace("\x1b[32m", "") - output = output.replace("\x1b[0m \n", "") - output = output.replace("[ERROR]", "") - err = new Error(output) - } - this.send(`${this.name}:data`, output) - }) - - flasher.stderr.on('data', (data: any) => { - output = Buffer.from(data, 'utf-8').toString() - err = new Error(output) - this.send(`${this.name}:data`, output) - }) - - flasher.on('close', (code: any) => { - if (err) { - this.send(`${this.name}:error`, { was: 'wipe', done: false , name: err.name, message: err.message, stack: err.stack }) - } else { - this.send(`${this.name}:success`, { was: 'wipe', done: true }) - } - }) - }) - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 89232bea..00000000 --- a/package.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "name": "krux-installer", - "version": "0.0.14", - "main": "dist-electron/main/index.js", - "description": "Graphical User Interface to download, verify and flash Krux´s firmware on Kendryte K210 hardwares as bitcoin signature devices", - "author": "qlrd <106913782+qlrd@users.noreply.github.com>", - "license": "MIT", - "private": true, - "keywords": [ - "electron", - "krux", - "vite", - "vuetify", - "vue3", - "vue" - ], - "vscode": { - "debug": { - "env": { - "VITE_DEV_SERVER_URL": "http://127.0.0.1:3344/", - "WDIO_ELECTRON": false, - "DEBUG": "krux:*" - }, - "run": "yarn run dev" - }, - "e2e": { - "env": { - "DEBUG": "krux:*", - "NODE_ENV": "test" - }, - "run": "echo Running E2E" - } - }, - "scripts": { - "dev": "vue-tsc --noEmit && vite", - "build": "vue-tsc --noEmit && vite build && electron-builder", - "preview": "vite preview", - "e2e": "wdio run wdio.conf.mts", - "lint:readme": "markdownlint README.md --ignore node_modules", - "lint:changelog": "markdownlint CHANGELOG.md --ignore node_modules", - "lint:warning": "markdownlint WARNING.md --ignore node_modules", - "lint:todo": "markdownlint TODO.md --ignore node_modules", - "lint": "yarn run lint:readme && yarn run lint:changelog && yarn run lint:warning && yarn run lint:todo" - }, - "devDependencies": { - "@babel/cli": "^7.22.9", - "@babel/core": "^7.22.9", - "@babel/preset-env": "^7.22.9", - "@babel/register": "^7.22.5", - "@types/chai": "^4.3.5", - "@types/command-exists": "^1.2.0", - "@types/debug": "^4.1.7", - "@types/mocha": "^10.0.1", - "@types/node": "^20.10.5", - "@vitejs/plugin-vue": "^5.0.4", - "@wdio/cli": "^8.27.0", - "@wdio/globals": "^8.27.0", - "@wdio/local-runner": "^8.27.0", - "@wdio/mocha-framework": "^8.27.0", - "@wdio/spec-reporter": "^8.27.0", - "chai": "^5.1.0", - "electron": "^29.1.0", - "electron-builder": "^24.4.0", - "markdownlint-cli": "^0.39.0", - "mocha": "^10.2.0", - "os-lang": "^3.1.1", - "rimraf": "^5.0.1", - "ts-node": "^10.9.1", - "typescript": "^5.1.6", - "vite": "^5.0.10", - "vite-plugin-electron": "^0.28.2", - "vite-plugin-electron-renderer": "^0.14.1", - "vite-plugin-html": "^3.2.0", - "vue": "^3.3.13", - "vue-tsc": "^2.0.6", - "wdio-electron-service": "^6.0.2" - }, - "dependencies": { - "@o/electron-sudo": "^2.8.23", - "ansi_up": "^6.0.0", - "axios": "^1.4.0", - "command-exists": "^1.2.9", - "debug": "^4.3.4", - "electron-store": "^8.1.0", - "glob": "^10.3.3", - "serialport": "^12.0.0", - "vite-plugin-vuetify": "^2.0.1", - "vue-asciimorph": "^0.0.3", - "vuetify": "^3.4.8", - "yauzl": "^3.1.2" - } -} diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..55f5a3c0 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1761 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "altgraph" +version = "0.17.4" +description = "Python graph (network) package" +optional = false +python-versions = "*" +files = [ + {file = "altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff"}, + {file = "altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406"}, +] + +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = "*" +files = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] + +[[package]] +name = "astroid" +version = "3.2.4" +description = "An abstract syntax tree for Python with inference support." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "astroid-3.2.4-py3-none-any.whl", hash = "sha256:413658a61eeca6202a59231abb473f932038fbcbf1666587f66d482083413a25"}, + {file = "astroid-3.2.4.tar.gz", hash = "sha256:0e14202810b30da1b735827f78f5157be2bbd4a7a59b7707ca0bfc2fb4c0063a"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} + +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "black" +version = "24.4.2" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, + {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, + {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, + {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, + {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, + {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, + {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, + {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, + {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, + {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, + {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, + {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, + {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, + {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, + {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, + {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, + {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, + {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, + {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, + {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, + {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, + {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "cachecontrol" +version = "0.14.0" +description = "httplib2 caching for requests" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cachecontrol-0.14.0-py3-none-any.whl", hash = "sha256:f5bf3f0620c38db2e5122c0726bdebb0d16869de966ea6a2befe92470b740ea0"}, + {file = "cachecontrol-0.14.0.tar.gz", hash = "sha256:7db1195b41c81f8274a7bbd97c956f44e8348265a1bc7641c37dfebc39f0c938"}, +] + +[package.dependencies] +msgpack = ">=0.5.2,<2.0.0" +requests = ">=2.16.0" + +[package.extras] +dev = ["CacheControl[filecache,redis]", "black", "build", "cherrypy", "furo", "mypy", "pytest", "pytest-cov", "sphinx", "sphinx-copybutton", "tox", "types-redis", "types-requests"] +filecache = ["filelock (>=3.8.0)"] +redis = ["redis (>=2.10.5)"] + +[[package]] +name = "cached-property" +version = "1.5.2" +description = "A decorator for caching properties in classes." +optional = false +python-versions = "*" +files = [ + {file = "cached-property-1.5.2.tar.gz", hash = "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130"}, + {file = "cached_property-1.5.2-py2.py3-none-any.whl", hash = "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"}, +] + +[[package]] +name = "certifi" +version = "2024.7.4" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, +] + +[[package]] +name = "cffi" +version = "1.16.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.6.0" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dff044f661f59dace805eedb4a7404c573b6ff0cdba4a524141bc63d7be5c7fd"}, + {file = "coverage-7.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8659fd33ee9e6ca03950cfdcdf271d645cf681609153f218826dd9805ab585c"}, + {file = "coverage-7.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7792f0ab20df8071d669d929c75c97fecfa6bcab82c10ee4adb91c7a54055463"}, + {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b3cd1ca7cd73d229487fa5caca9e4bc1f0bca96526b922d61053ea751fe791"}, + {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7e128f85c0b419907d1f38e616c4f1e9f1d1b37a7949f44df9a73d5da5cd53c"}, + {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a94925102c89247530ae1dab7dc02c690942566f22e189cbd53579b0693c0783"}, + {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dcd070b5b585b50e6617e8972f3fbbee786afca71b1936ac06257f7e178f00f6"}, + {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d50a252b23b9b4dfeefc1f663c568a221092cbaded20a05a11665d0dbec9b8fb"}, + {file = "coverage-7.6.0-cp310-cp310-win32.whl", hash = "sha256:0e7b27d04131c46e6894f23a4ae186a6a2207209a05df5b6ad4caee6d54a222c"}, + {file = "coverage-7.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:54dece71673b3187c86226c3ca793c5f891f9fc3d8aa183f2e3653da18566169"}, + {file = "coverage-7.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7b525ab52ce18c57ae232ba6f7010297a87ced82a2383b1afd238849c1ff933"}, + {file = "coverage-7.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bea27c4269234e06f621f3fac3925f56ff34bc14521484b8f66a580aacc2e7d"}, + {file = "coverage-7.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed8d1d1821ba5fc88d4a4f45387b65de52382fa3ef1f0115a4f7a20cdfab0e94"}, + {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01c322ef2bbe15057bc4bf132b525b7e3f7206f071799eb8aa6ad1940bcf5fb1"}, + {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03cafe82c1b32b770a29fd6de923625ccac3185a54a5e66606da26d105f37dac"}, + {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d1b923fc4a40c5832be4f35a5dab0e5ff89cddf83bb4174499e02ea089daf57"}, + {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4b03741e70fb811d1a9a1d75355cf391f274ed85847f4b78e35459899f57af4d"}, + {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a73d18625f6a8a1cbb11eadc1d03929f9510f4131879288e3f7922097a429f63"}, + {file = "coverage-7.6.0-cp311-cp311-win32.whl", hash = "sha256:65fa405b837060db569a61ec368b74688f429b32fa47a8929a7a2f9b47183713"}, + {file = "coverage-7.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:6379688fb4cfa921ae349c76eb1a9ab26b65f32b03d46bb0eed841fd4cb6afb1"}, + {file = "coverage-7.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f7db0b6ae1f96ae41afe626095149ecd1b212b424626175a6633c2999eaad45b"}, + {file = "coverage-7.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bbdf9a72403110a3bdae77948b8011f644571311c2fb35ee15f0f10a8fc082e8"}, + {file = "coverage-7.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc44bf0315268e253bf563f3560e6c004efe38f76db03a1558274a6e04bf5d5"}, + {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da8549d17489cd52f85a9829d0e1d91059359b3c54a26f28bec2c5d369524807"}, + {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0086cd4fc71b7d485ac93ca4239c8f75732c2ae3ba83f6be1c9be59d9e2c6382"}, + {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fad32ee9b27350687035cb5fdf9145bc9cf0a094a9577d43e909948ebcfa27b"}, + {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:044a0985a4f25b335882b0966625270a8d9db3d3409ddc49a4eb00b0ef5e8cee"}, + {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:76d5f82213aa78098b9b964ea89de4617e70e0d43e97900c2778a50856dac605"}, + {file = "coverage-7.6.0-cp312-cp312-win32.whl", hash = "sha256:3c59105f8d58ce500f348c5b56163a4113a440dad6daa2294b5052a10db866da"}, + {file = "coverage-7.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca5d79cfdae420a1d52bf177de4bc2289c321d6c961ae321503b2ca59c17ae67"}, + {file = "coverage-7.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d39bd10f0ae453554798b125d2f39884290c480f56e8a02ba7a6ed552005243b"}, + {file = "coverage-7.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beb08e8508e53a568811016e59f3234d29c2583f6b6e28572f0954a6b4f7e03d"}, + {file = "coverage-7.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2e16f4cd2bc4d88ba30ca2d3bbf2f21f00f382cf4e1ce3b1ddc96c634bc48ca"}, + {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6616d1c9bf1e3faea78711ee42a8b972367d82ceae233ec0ac61cc7fec09fa6b"}, + {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4567d6c334c46046d1c4c20024de2a1c3abc626817ae21ae3da600f5779b44"}, + {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d17c6a415d68cfe1091d3296ba5749d3d8696e42c37fca5d4860c5bf7b729f03"}, + {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9146579352d7b5f6412735d0f203bbd8d00113a680b66565e205bc605ef81bc6"}, + {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cdab02a0a941af190df8782aafc591ef3ad08824f97850b015c8c6a8b3877b0b"}, + {file = "coverage-7.6.0-cp38-cp38-win32.whl", hash = "sha256:df423f351b162a702c053d5dddc0fc0ef9a9e27ea3f449781ace5f906b664428"}, + {file = "coverage-7.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:f2501d60d7497fd55e391f423f965bbe9e650e9ffc3c627d5f0ac516026000b8"}, + {file = "coverage-7.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7221f9ac9dad9492cecab6f676b3eaf9185141539d5c9689d13fd6b0d7de840c"}, + {file = "coverage-7.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ddaaa91bfc4477d2871442bbf30a125e8fe6b05da8a0015507bfbf4718228ab2"}, + {file = "coverage-7.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4cbe651f3904e28f3a55d6f371203049034b4ddbce65a54527a3f189ca3b390"}, + {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:831b476d79408ab6ccfadaaf199906c833f02fdb32c9ab907b1d4aa0713cfa3b"}, + {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46c3d091059ad0b9c59d1034de74a7f36dcfa7f6d3bde782c49deb42438f2450"}, + {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4d5fae0a22dc86259dee66f2cc6c1d3e490c4a1214d7daa2a93d07491c5c04b6"}, + {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:07ed352205574aad067482e53dd606926afebcb5590653121063fbf4e2175166"}, + {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:49c76cdfa13015c4560702574bad67f0e15ca5a2872c6a125f6327ead2b731dd"}, + {file = "coverage-7.6.0-cp39-cp39-win32.whl", hash = "sha256:482855914928c8175735a2a59c8dc5806cf7d8f032e4820d52e845d1f731dca2"}, + {file = "coverage-7.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:543ef9179bc55edfd895154a51792b01c017c87af0ebaae092720152e19e42ca"}, + {file = "coverage-7.6.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:6fe885135c8a479d3e37a7aae61cbd3a0fb2deccb4dda3c25f92a49189f766d6"}, + {file = "coverage-7.6.0.tar.gz", hash = "sha256:289cc803fa1dc901f84701ac10c9ee873619320f2f9aff38794db4a4a0268d51"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "cryptography" +version = "42.0.8" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e"}, + {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7"}, + {file = "cryptography-42.0.8-cp37-abi3-win32.whl", hash = "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2"}, + {file = "cryptography-42.0.8-cp37-abi3-win_amd64.whl", hash = "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba"}, + {file = "cryptography-42.0.8-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14"}, + {file = "cryptography-42.0.8-cp39-abi3-win32.whl", hash = "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c"}, + {file = "cryptography-42.0.8-cp39-abi3-win_amd64.whl", hash = "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad"}, + {file = "cryptography-42.0.8.tar.gz", hash = "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "demjson3" +version = "3.0.6" +description = "encoder, decoder, and lint/validator for JSON (JavaScript Object Notation) compliant with RFC 7159" +optional = false +python-versions = "*" +files = [ + {file = "demjson3-3.0.6.tar.gz", hash = "sha256:37c83b0c6eb08d25defc88df0a2a4875d58a7809a9650bd6eee7afd8053cdbac"}, +] + +[[package]] +name = "dill" +version = "0.3.8" +description = "serialize all of Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, + {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] +profile = ["gprof2dot (>=2022.7.29)"] + +[[package]] +name = "distro" +version = "1.9.0" +description = "Distro - an OS platform information API" +optional = false +python-versions = ">=3.6" +files = [ + {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, + {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, +] + +[[package]] +name = "docutils" +version = "0.21.2" +description = "Docutils -- Python Documentation Utilities" +optional = false +python-versions = ">=3.9" +files = [ + {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, + {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, +] + +[[package]] +name = "easy-i18n" +version = "1.2.0" +description = "an easy way come true i18n for python3" +optional = false +python-versions = "*" +files = [ + {file = "easy_i18n-1.2.0-py3-none-any.whl", hash = "sha256:8aa94662dc0460bcd0a4004057643863a88b5dc89ac191950ac8097bada3e999"}, + {file = "easy_i18n-1.2.0.tar.gz", hash = "sha256:0652017bd7f8c2f8902b334189640c1578a276de69383d7499ed81543e856f76"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "feedparser" +version = "6.0.11" +description = "Universal feed parser, handles RSS 0.9x, RSS 1.0, RSS 2.0, CDF, Atom 0.3, and Atom 1.0 feeds" +optional = false +python-versions = ">=3.6" +files = [ + {file = "feedparser-6.0.11-py3-none-any.whl", hash = "sha256:0be7ee7b395572b19ebeb1d6aafb0028dee11169f1c934e0ed67d54992f4ad45"}, + {file = "feedparser-6.0.11.tar.gz", hash = "sha256:c9d0407b64c6f2a065d0ebb292c2b35c01050cc0dc33757461aaabdc4c4184d5"}, +] + +[package.dependencies] +sgmllib3k = "*" + +[[package]] +name = "filelock" +version = "3.0.12" +description = "A platform independent file lock." +optional = false +python-versions = "*" +files = [ + {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, + {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, +] + +[[package]] +name = "idna" +version = "3.7" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[package]] +name = "importlib-metadata" +version = "8.1.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-8.1.0-py3-none-any.whl", hash = "sha256:3cd29f739ed65973840b068e3132135ce954c254d48b5b640484467ef7ab3c8c"}, + {file = "importlib_metadata-8.1.0.tar.gz", hash = "sha256:fcdcb1d5ead7bdf3dd32657bb94ebe9d2aabfe89a19782ddc32da5041d6ebfb4"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "isort" +version = "5.13.2" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, +] + +[package.extras] +colors = ["colorama (>=0.4.6)"] + +[[package]] +name = "kivy" +version = "2.3.0" +description = "An open-source Python framework for developing GUI apps that work cross-platform, including desktop, mobile and embedded platforms." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Kivy-2.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcd8fdc742ae10d27e578df2052b4c3e99a754e91baad77d1f2e4f4d1238917f"}, + {file = "Kivy-2.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7492593a5d5d916c48b14a06fbe177341b1efb5753c9984be2fb84e3b3313c89"}, + {file = "Kivy-2.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:199c30e8daeace61392329766eeb68daa49631cd9793bec9440dda5cf30d68d5"}, + {file = "Kivy-2.3.0-cp310-cp310-win32.whl", hash = "sha256:03fc4b26c7d6a5ecee2c97ffa8d622e97ac8a8c4e0a00d333c156d64e09e4e19"}, + {file = "Kivy-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:aa5d57494cab405d395d65570d8481ab87869ba6daf4efb6c985bd16b32e7abf"}, + {file = "Kivy-2.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ec36ab3b74a525fa463b61895d3a2d76e9e4d206641233defae0d604e75df7ad"}, + {file = "Kivy-2.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd3e923397779776ac97ad87a1b9dd603b7f1c911a6ae04f1d1658712eaaf7cb"}, + {file = "Kivy-2.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7766baac2509d699df84b284579fa25ee31383d48893660cd8dba62081453a29"}, + {file = "Kivy-2.3.0-cp311-cp311-win32.whl", hash = "sha256:d654aaec6ddf9ca0edf73abd79e6aea423299c825a7ac432df17b031adaa7900"}, + {file = "Kivy-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:33dca85a520fe958e7134b96025b0625eb769adfb8829359959c8b314b7bc8d4"}, + {file = "Kivy-2.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7b1307521843d316265481d963344e85870ae5fa0c7d0881129749acfe61da7b"}, + {file = "Kivy-2.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:521105a4ca1db3e1203c3cdba4abe737533874d9c29bbfb1e1ae941238507440"}, + {file = "Kivy-2.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6672959894f652856d1dfcbcdcc09263de5f1cbed768b997dc8dcecab4385a4f"}, + {file = "Kivy-2.3.0-cp312-cp312-win32.whl", hash = "sha256:cf0bccc95b1344b79fbfdf54155d40438490f9801fd77279f068a4f66db72e4e"}, + {file = "Kivy-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:710648c987a63e37c723e6622853efe0278767596631a38728a54474b2cb77f2"}, + {file = "Kivy-2.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d2c6a411e2d837684d91b46231dd12db74fb1db6a2628e9f27581ce1583e5c8a"}, + {file = "Kivy-2.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8757f189d8a41a4b164150144037359405906a46b07572e8e1c602a782cacebf"}, + {file = "Kivy-2.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06c9b0a4bff825793e150e2cdbc823b59f635ce51e575d470d0fc3a06159596c"}, + {file = "Kivy-2.3.0-cp37-cp37m-win32.whl", hash = "sha256:d72599b80c8a7c2698769b4129ff52f2c4e28b6a75f9401180052c7d80763f19"}, + {file = "Kivy-2.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:0c42cf3c33e1aa3dee9c8acb6f91f8a4ad6c9de76064dcb8fdb1c60809643788"}, + {file = "Kivy-2.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:da8dd7ade7b7859642f53c3f32e10513877ce650367b68591b3aaacb46dcf012"}, + {file = "Kivy-2.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb6191bb51983f9e8257356aa53a71ccff5b6cf92f0bdcd5756973a6ac4b4446"}, + {file = "Kivy-2.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa3e7ce4fbd22284b303939676c5ae5448bb1e4d405f066dfc76c7cf56595cd"}, + {file = "Kivy-2.3.0-cp38-cp38-win32.whl", hash = "sha256:221f809220e518ae8b88a9b31310f9fef73727569e5cb13436572674fce4507b"}, + {file = "Kivy-2.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:8d2c3e5927fcf021d32124f915d56ae29e3a126c4f53db098436ea3959758a4c"}, + {file = "Kivy-2.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e8b91dfa2ad83739cc12d0f7bbe6410a3af2c2b3afd7b1d08919d9ec92826d61"}, + {file = "Kivy-2.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d79cb4a8649c476db18a079c447e57f8dbd4ad41459dc2162133a45cbb8cae96"}, + {file = "Kivy-2.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3be8db1eecc2d18859a7324b5cea79afb44095ccd73671987840afa26c68b0c9"}, + {file = "Kivy-2.3.0-cp39-cp39-win32.whl", hash = "sha256:5e6c431088584132d685696592e281fac217a5fd662f92cc6c6b48316e30b9c2"}, + {file = "Kivy-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:c332ff319db7648004d486c40fc4e700972f8e79a882d698e18eb238b2009e98"}, + {file = "Kivy-2.3.0.tar.gz", hash = "sha256:e8b8610c7f8ef6db908a139d369b247378f18105c96981e492eab2b4706c79d5"}, +] + +[package.dependencies] +docutils = "*" +"kivy-deps.angle" = {version = ">=0.4.0,<0.5.0", markers = "sys_platform == \"win32\""} +"kivy-deps.glew" = {version = ">=0.3.1,<0.4.0", markers = "sys_platform == \"win32\""} +"kivy-deps.sdl2" = {version = ">=0.7.0,<0.8.0", markers = "sys_platform == \"win32\""} +Kivy-Garden = ">=0.1.4" +pygments = "*" +pypiwin32 = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +angle = ["kivy-deps.angle (>=0.4.0,<0.5.0)"] +base = ["docutils", "kivy-deps.angle (>=0.4.0,<0.5.0)", "kivy-deps.glew (>=0.3.1,<0.4.0)", "kivy-deps.sdl2 (>=0.7.0,<0.8.0)", "pillow (>=9.5.0,<11)", "pygments", "pypiwin32", "requests"] +dev = ["flake8", "funcparserlib (==1.0.0a0)", "kivy-deps.glew-dev (>=0.3.1,<0.4.0)", "kivy-deps.gstreamer-dev (>=0.3.3,<0.4.0)", "kivy-deps.sdl2-dev (>=0.7.0,<0.8.0)", "pre-commit", "pyinstaller", "pytest (>=3.6)", "pytest-asyncio (!=0.11.0)", "pytest-benchmark", "pytest-cov", "pytest-timeout", "responses", "sphinx (<=6.2.1)", "sphinxcontrib-actdiag", "sphinxcontrib-blockdiag", "sphinxcontrib-jquery", "sphinxcontrib-nwdiag", "sphinxcontrib-seqdiag"] +full = ["docutils", "ffpyplayer", "kivy-deps.angle (>=0.4.0,<0.5.0)", "kivy-deps.glew (>=0.3.1,<0.4.0)", "kivy-deps.gstreamer (>=0.3.3,<0.4.0)", "kivy-deps.sdl2 (>=0.7.0,<0.8.0)", "pillow (>=9.5.0,<11)", "pygments", "pypiwin32"] +glew = ["kivy-deps.glew (>=0.3.1,<0.4.0)"] +gstreamer = ["kivy-deps.gstreamer (>=0.3.3,<0.4.0)"] +media = ["ffpyplayer", "kivy-deps.gstreamer (>=0.3.3,<0.4.0)"] +sdl2 = ["kivy-deps.sdl2 (>=0.7.0,<0.8.0)"] +tuio = ["oscpy"] + +[[package]] +name = "kivy-deps-angle" +version = "0.4.0" +description = "Repackaged binary dependency of Kivy." +optional = false +python-versions = "*" +files = [ + {file = "kivy_deps.angle-0.4.0-cp310-cp310-win32.whl", hash = "sha256:7873a551e488afa5044c4949a4aa42c4a4c4290469f0a6dd861e6b95283c9638"}, + {file = "kivy_deps.angle-0.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:71f2f01a3a7bbe1d4790e2a64e64a0ea8ae154418462ea407799ed66898b2c1f"}, + {file = "kivy_deps.angle-0.4.0-cp311-cp311-win32.whl", hash = "sha256:c3899ff1f3886b80b155955bad07bfa33bbebd97718cdf46dfd788dc467124bc"}, + {file = "kivy_deps.angle-0.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:574381d4e66f3198bc48aa10f238e7a3816ad56b80ec939f5d56fb33a378d0b1"}, + {file = "kivy_deps.angle-0.4.0-cp312-cp312-win32.whl", hash = "sha256:4fa7a6366899fba13f7624baf4645787165f45731db08d14557da29c12ee48f0"}, + {file = "kivy_deps.angle-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:668e670d4afd2551af0af2c627ceb0feac884bd799fb6a3dff78fdbfa2ea0451"}, + {file = "kivy_deps.angle-0.4.0-cp37-cp37m-win32.whl", hash = "sha256:24cfc0076d558080a00c443c7117311b4a977c1916fe297232eff1fd6f62651e"}, + {file = "kivy_deps.angle-0.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:48592ac6f7c183c5cd10d9ebe43d4148d0b2b9e400a2b0bcb5d21014cc929ce2"}, + {file = "kivy_deps.angle-0.4.0-cp38-cp38-win32.whl", hash = "sha256:1bbacf20bf6bd6ee965388f95d937c8fba2c54916fb44faa166c2ba58276753c"}, + {file = "kivy_deps.angle-0.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:e2ba4e390b02ad5bcb57b43a9227fa27ff55e69cd715a87217b324195eb267c3"}, + {file = "kivy_deps.angle-0.4.0-cp39-cp39-win32.whl", hash = "sha256:6546a62aba2b7e18a800b3df79daa757af3a980c297646c986896522395794e2"}, + {file = "kivy_deps.angle-0.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:bfaf9b37f2ecc3e4e7736657eed507716477af35cdd3118903e999d9d567ae8c"}, +] + +[[package]] +name = "kivy-deps-glew" +version = "0.3.1" +description = "Repackaged binary dependency of Kivy." +optional = false +python-versions = "*" +files = [ + {file = "kivy_deps.glew-0.3.1-cp310-cp310-win32.whl", hash = "sha256:8f4b3ed15acb62474909b6d41661ffb4da9eb502bb5684301fb2da668f288a58"}, + {file = "kivy_deps.glew-0.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:aef2d2a93f129d8425c75234e7f6cc0a34b59a4aee67f6d2cd7a5fdfa9915b53"}, + {file = "kivy_deps.glew-0.3.1-cp311-cp311-win32.whl", hash = "sha256:ee2f80ef7ac70f4b61c50da8101b024308a8c59a57f7f25a6e09762b6c48f942"}, + {file = "kivy_deps.glew-0.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:22e155ec59ce717387f5d8804811206d200a023ba3d0bc9bbf1393ee28d0053e"}, + {file = "kivy_deps.glew-0.3.1-cp312-cp312-win32.whl", hash = "sha256:b64ee4e445a04bc7c848c0261a6045fc2f0944cc05d7f953e3860b49f2703424"}, + {file = "kivy_deps.glew-0.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:3acbbd30da05fc10c185b5d4bb75fbbc882a6ef2192963050c1c94d60a6e795a"}, + {file = "kivy_deps.glew-0.3.1-cp37-cp37m-win32.whl", hash = "sha256:5bf6a63fe9cc4fe7bbf280ec267ec8c47914020a1175fb22152525ff1837b436"}, + {file = "kivy_deps.glew-0.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:d64a8625799fab7a7efeb3661ef8779a7f9c6d80da53eed87a956320f55530fa"}, + {file = "kivy_deps.glew-0.3.1-cp38-cp38-win32.whl", hash = "sha256:00f4ae0a4682d951266458ddb639451edb24baa54a35215dce889209daf19a06"}, + {file = "kivy_deps.glew-0.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f8b89dcf1846032d7a9c5ef88b0ee9cbd13366e9b4c85ada61e01549a910677"}, + {file = "kivy_deps.glew-0.3.1-cp39-cp39-win32.whl", hash = "sha256:4e377ed97670dfda619a1b63a82345a8589be90e7c616a458fba2810708810b1"}, + {file = "kivy_deps.glew-0.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:081a09b92f7e7817f489f8b6b31c9c9623661378de1dce1d6b097af5e7d42b45"}, +] + +[[package]] +name = "kivy-deps-sdl2" +version = "0.7.0" +description = "Repackaged binary dependency of Kivy." +optional = false +python-versions = "*" +files = [ + {file = "kivy_deps.sdl2-0.7.0-cp310-cp310-win32.whl", hash = "sha256:3c4b2bf1e473e6124563e1ff58cf3475c4f19fe9248940872c9e3c248bac3cb4"}, + {file = "kivy_deps.sdl2-0.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:ac0f4a6fe989899a60bbdb39516f45e4d90e2499864ab5d63e3706001cde48e8"}, + {file = "kivy_deps.sdl2-0.7.0-cp311-cp311-win32.whl", hash = "sha256:b727123d059c0c00c7d13cc1db8c8cfd0e48388cf24c11ec71cc6783811063c8"}, + {file = "kivy_deps.sdl2-0.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd946ca4e36a403bcafbe202033948c17f54bd5d28a343d98efd61f976822855"}, + {file = "kivy_deps.sdl2-0.7.0-cp312-cp312-win32.whl", hash = "sha256:2a8f23fe201dea368b47adfecf8fb9133315788d314ad32f33000254aa2388e4"}, + {file = "kivy_deps.sdl2-0.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:e56d5d651f81545c24f920f6f6e5d67b4100802152521022ccde53e822c507a2"}, + {file = "kivy_deps.sdl2-0.7.0-cp37-cp37m-win32.whl", hash = "sha256:c75626f6a3f8979b1c6a59e5070c7a547bb7c379a8e03f249af6b4c399305fc1"}, + {file = "kivy_deps.sdl2-0.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:95005fb3ae5b9e1d5edd32a6c0cfae9019efa2aeb3d909738dd73c5b9eea9dc1"}, + {file = "kivy_deps.sdl2-0.7.0-cp38-cp38-win32.whl", hash = "sha256:9728eaf70af514e0df163b062944fec008a5ceb73e53897ac89e62fcd2b0bac2"}, + {file = "kivy_deps.sdl2-0.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:a23811df7359e62acf4002fe5240d968a25e7aeaf7989b78b59cd6437f34f7b9"}, + {file = "kivy_deps.sdl2-0.7.0-cp39-cp39-win32.whl", hash = "sha256:ecbbcbd562a14a4a3870c8b6a0b1612eda24e9435df74fbb8e5f670560f0a9d6"}, + {file = "kivy_deps.sdl2-0.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:a5ef494d2f57224b93649df5f7a20c4f4cbc22416167732bf9f62d1cb263fef4"}, +] + +[[package]] +name = "kivy-garden" +version = "0.1.5" +description = "" +optional = false +python-versions = "*" +files = [ + {file = "Kivy Garden-0.1.5.tar.gz", hash = "sha256:2b8377378e87501d5d271f33d94f0e44c089884572c64f89c9d609b1f86a2748"}, + {file = "Kivy_Garden-0.1.5-py3-none-any.whl", hash = "sha256:ef50f44b96358cf10ac5665f27a4751bb34ef54051c54b93af891f80afe42929"}, +] + +[package.dependencies] +requests = "*" + +[[package]] +name = "kivysome" +version = "0.2.1" +description = "Font Awesome 5 Icons for Kivy" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "kivysome-0.2.1-py3-none-any.whl", hash = "sha256:30e1063aecd8d74ac4e04af7530ffbfceedfeac69f97a0dc04d856f61a22775d"}, + {file = "kivysome-0.2.1.tar.gz", hash = "sha256:e6510822adebac632325ef2d3ec63aae6c9e3d9246c598ca9a64156fe2a5bd9a"}, +] + +[package.dependencies] +lastversion = "1.2.0" +mutapath = ">=0.16.0,<0.17.0" +remotezip = ">=0.9.2,<0.10.0" +semver = ">=2.10.2,<3.0.0" +urllib3 = ">=1.25.7,<2.0.0" + +[[package]] +name = "lastversion" +version = "1.2.0" +description = "A CLI tool to fetch last GitHub release version" +optional = false +python-versions = "*" +files = [ + {file = "lastversion-1.2.0-py3-none-any.whl", hash = "sha256:375bccba97d447f031c13f6dcc613ee6829727613edea7ee28bb8b3686c1ddc0"}, + {file = "lastversion-1.2.0.tar.gz", hash = "sha256:0b72b1a103ed2f7522a7147d79640b19884bb8b1b31ef16963d9eddbd35c1564"}, +] + +[package.dependencies] +appdirs = "*" +beautifulsoup4 = "*" +cachecontrol = "*" +feedparser = "*" +lockfile = "*" +packaging = "*" +python-dateutil = "*" +PyYAML = "*" +requests = ">=2.6.1" +six = "*" +tqdm = "*" + +[package.extras] +tests = ["PyYAML", "appdirs", "beautifulsoup4", "cachecontrol", "feedparser", "flake8", "lockfile", "packaging", "pytest (>=4.4.0)", "pytest-xdist", "python-dateutil", "requests (>=2.6.1)", "six", "tqdm"] + +[[package]] +name = "lockfile" +version = "0.12.2" +description = "Platform-independent file locking module" +optional = false +python-versions = "*" +files = [ + {file = "lockfile-0.12.2-py2.py3-none-any.whl", hash = "sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa"}, + {file = "lockfile-0.12.2.tar.gz", hash = "sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799"}, +] + +[[package]] +name = "macholib" +version = "1.16.3" +description = "Mach-O header analysis and editing" +optional = false +python-versions = "*" +files = [ + {file = "macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c"}, + {file = "macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30"}, +] + +[package.dependencies] +altgraph = ">=0.17" + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "msgpack" +version = "1.0.8" +description = "MessagePack serializer" +optional = false +python-versions = ">=3.8" +files = [ + {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653"}, + {file = "msgpack-1.0.8-cp310-cp310-win32.whl", hash = "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693"}, + {file = "msgpack-1.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce"}, + {file = "msgpack-1.0.8-cp311-cp311-win32.whl", hash = "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305"}, + {file = "msgpack-1.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543"}, + {file = "msgpack-1.0.8-cp312-cp312-win32.whl", hash = "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c"}, + {file = "msgpack-1.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a"}, + {file = "msgpack-1.0.8-cp38-cp38-win32.whl", hash = "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c"}, + {file = "msgpack-1.0.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, + {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, + {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, + {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, +] + +[[package]] +name = "mutapath" +version = "0.16.2" +description = "mutable pathlib" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "mutapath-0.16.2-py3-none-any.whl", hash = "sha256:72b6f2c7ac435356749efae083335cae12029740a1e0656356ab16ea0c37732d"}, + {file = "mutapath-0.16.2.tar.gz", hash = "sha256:c7c0a1a29c6e8e78b3c686fdd8acdd1a852e71dd011576176ca255c926e24c47"}, +] + +[package.dependencies] +cached-property = "1.5.2" +filelock = "3.0.12" +path = ">=13.1,<16.0" +singletons = "0.2.5" + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "numpy" +version = "2.0.1" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fbb536eac80e27a2793ffd787895242b7f18ef792563d742c2d673bfcb75134"}, + {file = "numpy-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:69ff563d43c69b1baba77af455dd0a839df8d25e8590e79c90fcbe1499ebde42"}, + {file = "numpy-2.0.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:1b902ce0e0a5bb7704556a217c4f63a7974f8f43e090aff03fcf262e0b135e02"}, + {file = "numpy-2.0.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:f1659887361a7151f89e79b276ed8dff3d75877df906328f14d8bb40bb4f5101"}, + {file = "numpy-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4658c398d65d1b25e1760de3157011a80375da861709abd7cef3bad65d6543f9"}, + {file = "numpy-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4127d4303b9ac9f94ca0441138acead39928938660ca58329fe156f84b9f3015"}, + {file = "numpy-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e5eeca8067ad04bc8a2a8731183d51d7cbaac66d86085d5f4766ee6bf19c7f87"}, + {file = "numpy-2.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9adbd9bb520c866e1bfd7e10e1880a1f7749f1f6e5017686a5fbb9b72cf69f82"}, + {file = "numpy-2.0.1-cp310-cp310-win32.whl", hash = "sha256:7b9853803278db3bdcc6cd5beca37815b133e9e77ff3d4733c247414e78eb8d1"}, + {file = "numpy-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:81b0893a39bc5b865b8bf89e9ad7807e16717f19868e9d234bdaf9b1f1393868"}, + {file = "numpy-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75b4e316c5902d8163ef9d423b1c3f2f6252226d1aa5cd8a0a03a7d01ffc6268"}, + {file = "numpy-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6e4eeb6eb2fced786e32e6d8df9e755ce5be920d17f7ce00bc38fcde8ccdbf9e"}, + {file = "numpy-2.0.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1e01dcaab205fbece13c1410253a9eea1b1c9b61d237b6fa59bcc46e8e89343"}, + {file = "numpy-2.0.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a8fc2de81ad835d999113ddf87d1ea2b0f4704cbd947c948d2f5513deafe5a7b"}, + {file = "numpy-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a3d94942c331dd4e0e1147f7a8699a4aa47dffc11bf8a1523c12af8b2e91bbe"}, + {file = "numpy-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15eb4eca47d36ec3f78cde0a3a2ee24cf05ca7396ef808dda2c0ddad7c2bde67"}, + {file = "numpy-2.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b83e16a5511d1b1f8a88cbabb1a6f6a499f82c062a4251892d9ad5d609863fb7"}, + {file = "numpy-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f87fec1f9bc1efd23f4227becff04bd0e979e23ca50cc92ec88b38489db3b55"}, + {file = "numpy-2.0.1-cp311-cp311-win32.whl", hash = "sha256:36d3a9405fd7c511804dc56fc32974fa5533bdeb3cd1604d6b8ff1d292b819c4"}, + {file = "numpy-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:08458fbf403bff5e2b45f08eda195d4b0c9b35682311da5a5a0a0925b11b9bd8"}, + {file = "numpy-2.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6bf4e6f4a2a2e26655717a1983ef6324f2664d7011f6ef7482e8c0b3d51e82ac"}, + {file = "numpy-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6fddc5fe258d3328cd8e3d7d3e02234c5d70e01ebe377a6ab92adb14039cb4"}, + {file = "numpy-2.0.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5daab361be6ddeb299a918a7c0864fa8618af66019138263247af405018b04e1"}, + {file = "numpy-2.0.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:ea2326a4dca88e4a274ba3a4405eb6c6467d3ffbd8c7d38632502eaae3820587"}, + {file = "numpy-2.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529af13c5f4b7a932fb0e1911d3a75da204eff023ee5e0e79c1751564221a5c8"}, + {file = "numpy-2.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6790654cb13eab303d8402354fabd47472b24635700f631f041bd0b65e37298a"}, + {file = "numpy-2.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbab9fc9c391700e3e1287666dfd82d8666d10e69a6c4a09ab97574c0b7ee0a7"}, + {file = "numpy-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99d0d92a5e3613c33a5f01db206a33f8fdf3d71f2912b0de1739894668b7a93b"}, + {file = "numpy-2.0.1-cp312-cp312-win32.whl", hash = "sha256:173a00b9995f73b79eb0191129f2455f1e34c203f559dd118636858cc452a1bf"}, + {file = "numpy-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:bb2124fdc6e62baae159ebcfa368708867eb56806804d005860b6007388df171"}, + {file = "numpy-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bfc085b28d62ff4009364e7ca34b80a9a080cbd97c2c0630bb5f7f770dae9414"}, + {file = "numpy-2.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8fae4ebbf95a179c1156fab0b142b74e4ba4204c87bde8d3d8b6f9c34c5825ef"}, + {file = "numpy-2.0.1-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:72dc22e9ec8f6eaa206deb1b1355eb2e253899d7347f5e2fae5f0af613741d06"}, + {file = "numpy-2.0.1-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:ec87f5f8aca726117a1c9b7083e7656a9d0d606eec7299cc067bb83d26f16e0c"}, + {file = "numpy-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f682ea61a88479d9498bf2091fdcd722b090724b08b31d63e022adc063bad59"}, + {file = "numpy-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8efc84f01c1cd7e34b3fb310183e72fcdf55293ee736d679b6d35b35d80bba26"}, + {file = "numpy-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3fdabe3e2a52bc4eff8dc7a5044342f8bd9f11ef0934fcd3289a788c0eb10018"}, + {file = "numpy-2.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:24a0e1befbfa14615b49ba9659d3d8818a0f4d8a1c5822af8696706fbda7310c"}, + {file = "numpy-2.0.1-cp39-cp39-win32.whl", hash = "sha256:f9cf5ea551aec449206954b075db819f52adc1638d46a6738253a712d553c7b4"}, + {file = "numpy-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:e9e81fa9017eaa416c056e5d9e71be93d05e2c3c2ab308d23307a8bc4443c368"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:61728fba1e464f789b11deb78a57805c70b2ed02343560456190d0501ba37b0f"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:12f5d865d60fb9734e60a60f1d5afa6d962d8d4467c120a1c0cda6eb2964437d"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eacf3291e263d5a67d8c1a581a8ebbcfd6447204ef58828caf69a5e3e8c75990"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2c3a346ae20cfd80b6cfd3e60dc179963ef2ea58da5ec074fd3d9e7a1e7ba97f"}, + {file = "numpy-2.0.1.tar.gz", hash = "sha256:485b87235796410c3519a699cfe1faab097e509e90ebb05dcd098db2ae87e7b3"}, +] + +[[package]] +name = "opencv-python" +version = "4.10.0.84" +description = "Wrapper package for OpenCV python bindings." +optional = false +python-versions = ">=3.6" +files = [ + {file = "opencv-python-4.10.0.84.tar.gz", hash = "sha256:72d234e4582e9658ffea8e9cae5b63d488ad06994ef12d81dc303b17472f3526"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fc182f8f4cda51b45f01c64e4cbedfc2f00aff799debebc305d8d0210c43f251"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:71e575744f1d23f79741450254660442785f45a0797212852ee5199ef12eed98"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09a332b50488e2dda866a6c5573ee192fe3583239fb26ff2f7f9ceb0bc119ea6"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ace140fc6d647fbe1c692bcb2abce768973491222c067c131d80957c595b71f"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-win32.whl", hash = "sha256:2db02bb7e50b703f0a2d50c50ced72e95c574e1e5a0bb35a8a86d0b35c98c236"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-win_amd64.whl", hash = "sha256:32dbbd94c26f611dc5cc6979e6b7aa1f55a64d6b463cc1dcd3c95505a63e48fe"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.21.0", markers = "python_version == \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""}, + {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""}, + {version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""}, + {version = ">=1.19.3", markers = "platform_system == \"Linux\" and platform_machine == \"aarch64\" and python_version >= \"3.8\" and python_version < \"3.10\" or python_version > \"3.9\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_system != \"Darwin\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_machine != \"arm64\" and python_version < \"3.10\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, + {version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, +] + +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "pastel" +version = "0.2.1" +description = "Bring colors to your terminal." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, + {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, +] + +[[package]] +name = "path" +version = "15.1.2" +description = "A module wrapper for os.path" +optional = false +python-versions = ">=3.6" +files = [ + {file = "path-15.1.2-py3-none-any.whl", hash = "sha256:e07601c83ae394cb05298c96e073c251d185599ecbf3cf7110b00f4d1898d53e"}, + {file = "path-15.1.2.tar.gz", hash = "sha256:bb629aefd86825bf21c8bcfa0f8691a6e5abdc3e43d50b626fe6aac5b13f60b7"}, +] + +[package.extras] +docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] +testing = ["appdirs", "packaging", "pygments", "pytest (>=3.5,!=3.7.3)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-enabler", "pytest-flake8", "pytest-mypy"] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "pefile" +version = "2023.2.7" +description = "Python PE parsing module" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"}, + {file = "pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc"}, +] + +[[package]] +name = "pillow" +version = "10.4.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, + {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"}, + {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"}, + {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"}, + {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"}, + {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"}, + {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"}, + {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"}, + {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"}, + {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"}, + {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"}, + {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"}, + {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"}, + {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"}, + {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"}, + {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"}, + {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"}, + {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"}, + {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"}, + {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + +[[package]] +name = "platformdirs" +version = "4.2.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "poethepoet" +version = "0.25.1" +description = "A task runner that works well with poetry." +optional = false +python-versions = ">=3.8" +files = [ + {file = "poethepoet-0.25.1-py3-none-any.whl", hash = "sha256:fee433f68424593bca6b357f0bf997d64edf42c7305c0d5d335bd570b8d2352b"}, + {file = "poethepoet-0.25.1.tar.gz", hash = "sha256:98f4446533a4b2bdb08843e211f918b1f2e7f8baf6d1803ef78f64661ed62463"}, +] + +[package.dependencies] +pastel = ">=0.2.1,<0.3.0" +tomli = ">=1.2.2" + +[package.extras] +poetry-plugin = ["poetry (>=1.0,<2.0)"] + +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pyinstaller" +version = "6.9.0" +description = "PyInstaller bundles a Python application and all its dependencies into a single package." +optional = false +python-versions = "<3.13,>=3.8" +files = [ + {file = "pyinstaller-6.9.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:5ced2e83acf222b936ea94abc5a5cc96588705654b39138af8fb321d9cf2b954"}, + {file = "pyinstaller-6.9.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:f18a3d551834ef8fb7830d48d4cc1527004d0e6b51ded7181e78374ad6111846"}, + {file = "pyinstaller-6.9.0-py3-none-manylinux2014_i686.whl", hash = "sha256:f2fc568de3d6d2a176716a3fc9f20da06d351e8bea5ddd10ecb5659fce3a05b0"}, + {file = "pyinstaller-6.9.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:a0f378f64ad0655d11ade9fde7877e7573fd3d5066231608ce7dfa9040faecdd"}, + {file = "pyinstaller-6.9.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:7bf0c13c5a8560c89540746ae742f4f4b82290e95a6b478374d9f34959fe25d6"}, + {file = "pyinstaller-6.9.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:da994aba14c5686db88796684de265a8665733b4df09b939f7ebdf097d18df72"}, + {file = "pyinstaller-6.9.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:4e3e50743c091a06e6d01c59bdd6d03967b453ee5384a9e790759be4129db4a4"}, + {file = "pyinstaller-6.9.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:b041be2fe78da47a269604d62c940d68c62f9a3913bdf64af4123f7689d47099"}, + {file = "pyinstaller-6.9.0-py3-none-win32.whl", hash = "sha256:2bf4de17a1c63c0b797b38e13bfb4d03b5ee7c0a68e28b915a7eaacf6b76087f"}, + {file = "pyinstaller-6.9.0-py3-none-win_amd64.whl", hash = "sha256:43709c70b1da8441a730327a8ed362bfcfdc3d42c1bf89f3e2b0a163cc4e7d33"}, + {file = "pyinstaller-6.9.0-py3-none-win_arm64.whl", hash = "sha256:f15c1ef11ed5ceb32447dfbdab687017d6adbef7fc32aa359d584369bfe56eda"}, + {file = "pyinstaller-6.9.0.tar.gz", hash = "sha256:f4a75c552facc2e2a370f1e422b971b5e5cdb4058ff38cea0235aa21fc0b378f"}, +] + +[package.dependencies] +altgraph = "*" +importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} +macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} +packaging = ">=22.0" +pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""} +pyinstaller-hooks-contrib = ">=2024.7" +pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""} +setuptools = ">=42.0.0" + +[package.extras] +completion = ["argcomplete"] +hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] + +[[package]] +name = "pyinstaller-hooks-contrib" +version = "2024.7" +description = "Community maintained hooks for PyInstaller" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyinstaller_hooks_contrib-2024.7-py2.py3-none-any.whl", hash = "sha256:8bf0775771fbaf96bcd2f4dfd6f7ae6c1dd1b1efe254c7e50477b3c08e7841d8"}, + {file = "pyinstaller_hooks_contrib-2024.7.tar.gz", hash = "sha256:fd5f37dcf99bece184e40642af88be16a9b89613ecb958a8bd1136634fc9fac5"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} +packaging = ">=22.0" +setuptools = ">=42.0.0" + +[[package]] +name = "pylint" +version = "3.2.6" +description = "python code static checker" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "pylint-3.2.6-py3-none-any.whl", hash = "sha256:03c8e3baa1d9fb995b12c1dbe00aa6c4bcef210c2a2634374aedeb22fb4a8f8f"}, + {file = "pylint-3.2.6.tar.gz", hash = "sha256:a5d01678349454806cff6d886fb072294f56a58c4761278c97fb557d708e1eb3"}, +] + +[package.dependencies] +astroid = ">=3.2.4,<=3.3.0-dev0" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +dill = [ + {version = ">=0.2", markers = "python_version < \"3.11\""}, + {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, + {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, +] +isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" +mccabe = ">=0.6,<0.8" +platformdirs = ">=2.2.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +tomlkit = ">=0.10.1" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} + +[package.extras] +spelling = ["pyenchant (>=3.2,<4.0)"] +testutils = ["gitpython (>3)"] + +[[package]] +name = "pypiwin32" +version = "223" +description = "" +optional = false +python-versions = "*" +files = [ + {file = "pypiwin32-223-py3-none-any.whl", hash = "sha256:67adf399debc1d5d14dffc1ab5acacb800da569754fafdc576b2a039485aa775"}, + {file = "pypiwin32-223.tar.gz", hash = "sha256:71be40c1fbd28594214ecaecb58e7aa8b708eabfa0125c8a109ebd51edbd776a"}, +] + +[package.dependencies] +pywin32 = ">=223" + +[[package]] +name = "pypng" +version = "0.20220715.0" +description = "Pure Python library for saving and loading PNG images" +optional = false +python-versions = "*" +files = [ + {file = "pypng-0.20220715.0-py3-none-any.whl", hash = "sha256:4a43e969b8f5aaafb2a415536c1a8ec7e341cd6a3f957fd5b5f32a4cfeed902c"}, + {file = "pypng-0.20220715.0.tar.gz", hash = "sha256:739c433ba96f078315de54c0db975aee537cbc3e1d0ae4ed9aab0ca1e427e2c1"}, +] + +[[package]] +name = "pyserial" +version = "3.5" +description = "Python Serial Port Extension" +optional = false +python-versions = "*" +files = [ + {file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"}, + {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"}, +] + +[package.extras] +cp2110 = ["hidapi"] + +[[package]] +name = "pysudoer" +version = "0.0.1" +description = "Run a subprocess with administrative privileges, prompting the user with a graphical OS dialog if necessary." +optional = false +python-versions = ">=3.9, <3.13" +files = [] +develop = false + +[package.source] +type = "git" +url = "https://github.com/qlrd/pysudoer.git" +reference = "HEAD" +resolved_reference = "47093f5eef1185e4e652c0ff7324678b01e3b677" + +[[package]] +name = "pytest" +version = "8.3.1" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.1-py3-none-any.whl", hash = "sha256:e9600ccf4f563976e2c99fa02c7624ab938296551f280835ee6516df8bc4ae8c"}, + {file = "pytest-8.3.1.tar.gz", hash = "sha256:7e8e5c5abd6e93cb1cc151f23e57adc31fcf8cfd2a3ff2da63e23f732de35db6"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pywin32" +version = "306" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +files = [ + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, + {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, + {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, + {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, + {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.2" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"}, + {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "pyzbar" +version = "0.1.9" +description = "Read one-dimensional barcodes and QR codes from Python 2 and 3." +optional = false +python-versions = "*" +files = [ + {file = "pyzbar-0.1.9-py2.py3-none-any.whl", hash = "sha256:4559628b8192feb25766d954b36a3753baaf5c97c03135aec7e4a026036b475d"}, + {file = "pyzbar-0.1.9-py2.py3-none-win32.whl", hash = "sha256:8f4c5264c9c7c6b9f20d01efc52a4eba1ded47d9ba857a94130afe33703eb518"}, + {file = "pyzbar-0.1.9-py2.py3-none-win_amd64.whl", hash = "sha256:13e3ee5a2f3a545204a285f41814d5c0db571967e8d4af8699a03afc55182a9c"}, +] + +[package.extras] +scripts = ["Pillow (>=3.2.0)"] + +[[package]] +name = "qrcode" +version = "7.4.2" +description = "QR Code image generator" +optional = false +python-versions = ">=3.7" +files = [ + {file = "qrcode-7.4.2-py3-none-any.whl", hash = "sha256:581dca7a029bcb2deef5d01068e39093e80ef00b4a61098a2182eac59d01643a"}, + {file = "qrcode-7.4.2.tar.gz", hash = "sha256:9dd969454827e127dbd93696b20747239e6d540e082937c90f14ac95b30f5845"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +pypng = "*" +typing-extensions = "*" + +[package.extras] +all = ["pillow (>=9.1.0)", "pytest", "pytest-cov", "tox", "zest.releaser[recommended]"] +dev = ["pytest", "pytest-cov", "tox"] +maintainer = ["zest.releaser[recommended]"] +pil = ["pillow (>=9.1.0)"] +test = ["coverage", "pytest"] + +[[package]] +name = "remotezip" +version = "0.9.4" +description = "Access zip file content hosted remotely without downloading the full file." +optional = false +python-versions = "*" +files = [ + {file = "remotezip-0.9.4.tar.gz", hash = "sha256:8bed7d1fd3f096c15e480d05492d84537ac401b473ba109e0b30611452ac8e57"}, +] + +[package.dependencies] +requests = "*" +tabulate = "*" + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "semver" +version = "2.13.0" +description = "Python helper for Semantic Versioning (http://semver.org/)" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "semver-2.13.0-py2.py3-none-any.whl", hash = "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4"}, + {file = "semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"}, +] + +[[package]] +name = "setuptools" +version = "71.1.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-71.1.0-py3-none-any.whl", hash = "sha256:33874fdc59b3188304b2e7c80d9029097ea31627180896fb549c578ceb8a0855"}, + {file = "setuptools-71.1.0.tar.gz", hash = "sha256:032d42ee9fb536e33087fb66cac5f840eb9391ed05637b3f2a76a7c8fb477936"}, +] + +[package.extras] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "sgmllib3k" +version = "1.0.0" +description = "Py3k port of sgmllib." +optional = false +python-versions = "*" +files = [ + {file = "sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9"}, +] + +[[package]] +name = "singletons" +version = "0.2.5" +description = "Singleton metaclasses and singleton factories" +optional = false +python-versions = ">=3.6,<4.0" +files = [ + {file = "singletons-0.2.5-py3-none-any.whl", hash = "sha256:d367554d86865b50fff5bc59cdc3a7a2c50d370ca405d0fe4161073458ffc78c"}, + {file = "singletons-0.2.5.tar.gz", hash = "sha256:17f827e4eb5aeae024509872325e78cdbb0c3d8183b763d2f3d3ae2bd5728f68"}, +] + +[package.extras] +eventlet = ["eventlet (>=0.25.1,<0.26.0)"] +gevent = ["gevent (>=1.4,<2.0)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "soupsieve" +version = "2.5" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, + {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, +] + +[[package]] +name = "tabulate" +version = "0.9.0" +description = "Pretty-print tabular data" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, + {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, +] + +[package.extras] +widechars = ["wcwidth"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "tomlkit" +version = "0.13.0" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomlkit-0.13.0-py3-none-any.whl", hash = "sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264"}, + {file = "tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72"}, +] + +[[package]] +name = "tqdm" +version = "4.66.4" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tqdm-4.66.4-py3-none-any.whl", hash = "sha256:b75ca56b413b030bc3f00af51fd2c1a1a5eac6a0c1cca83cbb37a5c52abce644"}, + {file = "tqdm-4.66.4.tar.gz", hash = "sha256:e4d936c9de8727928f3be6079590e97d9abfe8d39a590be678eb5919ffc186bb"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "urllib3" +version = "1.26.19" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3"}, + {file = "urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429"}, +] + +[package.extras] +brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "zipp" +version = "3.19.2" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, + {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, +] + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.9,<3.13" +content-hash = "257671a2870a76d77033dea2b60ead4decd34f399cb5ff13a22113f01b14508e" diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index 5fb479a1..00000000 Binary files a/public/favicon.ico and /dev/null differ diff --git a/public/icon.png b/public/icon.png deleted file mode 100644 index 411996cc..00000000 Binary files a/public/icon.png and /dev/null differ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..55a79f4a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,84 @@ +[tool.poetry] +name = "krux-installer" +version = "0.0.2-alpha" +description = "A GUI based application to flash Krux firmware on K210 based devices" +authors = [ + "qlrd " +] +license = "LICENSE" +readme = "README.md" + +[tool.poetry.dependencies] +python = ">=3.9,<3.13" +kivy = "2.3.0" +kivysome = "^0.2.1" +tomli = { version = "^2.0.1", python = "<3.11" } +pyserial = "^3.5" +requests = "^2.31.0" +pyzbar = "^0.1.9" +opencv-python = "^4.9.0.80" +cryptography = "^42.0.5" +qrcode = "^7.4.2" +easy-i18n = "^1.2.0" +pysudoer = {git = "https://github.com/qlrd/pysudoer.git"} +distro = "^1.9.0" +pillow = "^10.4.0" + +[tool.poetry.group.dev.dependencies] +black = "^24.1.1" +pylint = "^3.0.3" +pyinstaller = "^6.3.0" +pytest-cov = "^4.1.0" +poethepoet = "^0.25.1" +demjson3 = "^3.0.6" + +[tool.poe.tasks] +cli = "python src/krux-installer.py" +format-src= "black ./src" +format-tests= "black ./tests" +format-e2e= "black ./e2e" +format-installer = "black ./krux-installer.py" +format = ["format-src", "format-tests", "format-e2e", "format-installer"] + +test-unit = "pytest --cache-clear --cov=src/utils/constants --cov=src/utils/info --cov=src/utils/selector --cov=src/utils/downloader --cov=src/utils/trigger --cov=src/utils/flasher --cov=src/utils/unzip --cov=src/utils/signer --cov=src/utils/verifyer --cov=src/i18n --cov-branch --cov-report html ./tests" +test-e2e = "pytest --cov-append --cov=src/app --cov-branch --cov-report html ./e2e" +test = ["test-unit", "test-e2e"] + +coverage-unit = "pytest --cache-clear --cov=src/utils/constants --cov=src/utils/info --cov=src/utils/selector --cov=src/utils/downloader --cov=src/utils/trigger --cov=src/utils/flasher --cov=src/utils/unzip --cov=src/utils/signer --cov=src/utils/verifyer --cov=src/i18n --cov-branch --cov-report xml ./tests" +coverage-e2e = "pytest --cov-append --cov=src/app --cov-branch --cov-report xml ./e2e" +coverage = ["coverage-unit", "coverage-e2e"] + +patch-nix = "sh .ci/patch-pyinstaller-kivy-hook.sh" +patch-win = "powershell.exe -File .ci/patch-pyinstaller-kivy-hook.ps1" + +clean-mac = "find . -name '.DS_Store' -delete" + +lint.sequence = [ + { cmd = "jsonlint src/i18n/*.json"}, + { cmd = "pylint --rcfile .pylint/src ./src" }, + { cmd = "pylint --rcfile .pylint/tests ./tests"}, + { cmd = "pylint --rcfile=.pylint/tests ./e2e"} +] + +build-nix.sequence = [ + { cmd = "python .ci/create-spec.py"}, + { cmd = "python -m PyInstaller krux-installer.spec"} +] + +build-win.sequence = [ + { cmd = "python .ci/create-spec.py"}, + { interpreter = ["powershell", "pwsh"], shell = "& .ci/edit-spec.ps1"}, + { cmd = "python -m PyInstaller krux-installer.spec"} +] + +[tool.poe.tasks.dev-debug] +env = { LOGLEVEL = "debug" } +cmd = "python krux-installer.py" + +[tool.poe.tasks.dev] +env = { LOGLEVEL = "info" } +cmd = "python krux-installer.py" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/src/App.vue b/src/App.vue deleted file mode 100644 index f5371a2f..00000000 --- a/src/App.vue +++ /dev/null @@ -1,160 +0,0 @@ - - - - - diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/app/__init__.py b/src/app/__init__.py new file mode 100644 index 00000000..bbad55a8 --- /dev/null +++ b/src/app/__init__.py @@ -0,0 +1,134 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +__init__.py +""" +import os +import sys +from functools import partial +from kivy.clock import Clock +from kivy.core.window import Window +from src.app.config_krux_installer import ConfigKruxInstaller +from src.app.screens.greetings_screen import GreetingsScreen +from src.app.screens.check_permissions_screen import CheckPermissionsScreen +from src.app.screens.check_internet_connection_screen import ( + CheckInternetConnectionScreen, +) +from src.app.screens.main_screen import MainScreen +from src.app.screens.select_device_screen import SelectDeviceScreen +from src.app.screens.select_version_screen import SelectVersionScreen +from src.app.screens.select_old_version_screen import SelectOldVersionScreen +from src.app.screens.warning_beta_screen import WarningBetaScreen +from src.app.screens.about_screen import AboutScreen +from src.app.screens.download_stable_zip_screen import DownloadStableZipScreen +from src.app.screens.download_stable_zip_sha256_screen import ( + DownloadStableZipSha256Screen, +) +from src.app.screens.download_stable_zip_sig_screen import DownloadStableZipSigScreen +from src.app.screens.download_selfcustody_pem_screen import DownloadSelfcustodyPemScreen +from src.app.screens.verify_stable_zip_screen import VerifyStableZipScreen +from src.app.screens.unzip_stable_screen import UnzipStableScreen +from src.app.screens.download_beta_screen import DownloadBetaScreen +from src.app.screens.warning_already_downloaded_screen import ( + WarningAlreadyDownloadedScreen, +) +from src.app.screens.flash_screen import FlashScreen +from src.app.screens.warning_wipe_screen import WarningWipeScreen +from src.app.screens.wipe_screen import WipeScreen +from src.app.screens.error_screen import ErrorScreen + + +class KruxInstallerApp(ConfigKruxInstaller): + """KruxInstallerApp is the Root widget""" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + Window.maximize() + # Window.fullscreen = 'auto' + print(Window.size) + # Window.size = (640, 800) + self.debug(f"Window.size={Window.size}") + Window.clearcolor = (0.9, 0.9, 0.9, 1) + + def build(self): + """Create the Root widget with an ScreenManager as manager for its sub-widgets""" + self.setup_screens() + self.setup_screen_manager() + return self.screen_manager + + def on_start(self): + """When application starts, verify system and check latest firmware version""" + self.on_greetings() + + def setup_screen_manager(self): + """Loop through defined screens (if have at lease one) and add it to screen_manager""" + if len(self.screens) > 0: + for screen in self.screens: + msg = f"adding screen '{screen.name}'" + self.debug(msg) + self.screen_manager.add_widget(screen) + + else: + raise RuntimeError("Cannot setup screen_manager: screen list is empty") + + def setup_screens(self): + """Configure all screens given an OS""" + self.screens.append(GreetingsScreen()) + + if sys.platform == "linux": + self.screens.append(CheckPermissionsScreen()) + + self.screens = self.screens + [ + CheckInternetConnectionScreen(), + MainScreen(), + SelectDeviceScreen(), + SelectVersionScreen(), + SelectOldVersionScreen(), + WarningBetaScreen(), + AboutScreen(), + DownloadStableZipScreen(), + DownloadStableZipSha256Screen(), + DownloadStableZipSigScreen(), + DownloadSelfcustodyPemScreen(), + VerifyStableZipScreen(), + UnzipStableScreen(), + DownloadBetaScreen(), + WarningAlreadyDownloadedScreen(), + WarningWipeScreen(), + FlashScreen(), + WipeScreen(), + ErrorScreen(), + ] + + def on_greetings(self): + """ + When application start, after greeting user with the krux logo, it will need to check if + user is running app in linux or non-linux. If running in linux, the user will be + redirect to CheckPermissionsScreen and then to MainScreen. Win32 and Mac will be + redirect to MainScreen. + """ + + greetings_screen = self.screen_manager.get_screen("GreetingsScreen") + + fn = partial( + greetings_screen.update, name="KruxInstallerApp", key="check_permissions" + ) + Clock.schedule_once(fn, 0) diff --git a/src/app/base_krux_installer.py b/src/app/base_krux_installer.py new file mode 100644 index 00000000..9f0acf70 --- /dev/null +++ b/src/app/base_krux_installer.py @@ -0,0 +1,72 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +krux_installer.py +""" +import os +import typing +import tempfile +from kivy.logger import Logger, LOG_LEVELS +from kivy.app import App +from kivy.uix.screenmanager import Screen, ScreenManager +from ..utils.trigger import Trigger + +DEFAULT_DESTDIR = tempfile.mkdtemp() +DEFAULT_BAUDRATE = 1500000 +DEFAULT_LOCALE = "en-US" + + +class BaseKruxInstaller(App, Trigger): + """BaseKruxInstller is the base for Appliction""" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._screens = [] + self._screen_manager = ScreenManager() + + if "LOGLEVEL" in os.environ: + Logger.setLevel(LOG_LEVELS[os.environ["LOGLEVEL"]]) + else: + Logger.setLevel(LOG_LEVELS["info"]) + + @property + def screens(self) -> typing.List[Screen]: + """Getter of :class:`kivy.uix.screenmanager.Screen`""" + self.debug(f"screens::setter={self._screens}") + return self._screens + + @screens.setter + def screens(self, value: typing.List[Screen]): + """Setter of :class:`kivy.uix.screenmanager.Screen`""" + self.debug(f"screens::getter={value}") + self._screens = value + + @property + def screen_manager(self) -> ScreenManager: + """Getter of :class:`kivy.uix.screenmanager.ScreenManager`""" + self.debug(f"screen_manager::setter={self._screen_manager}") + return self._screen_manager + + @screen_manager.setter + def screen_manager(self, value: typing.List[Screen]): + """Setter of :class:`kivy.uix.screenmanager.ScreenManager`""" + self.debug(f"screen_manager::getter={value}") + self._screen_manager = value diff --git a/src/app/config_krux_installer.py b/src/app/config_krux_installer.py new file mode 100644 index 00000000..3565517d --- /dev/null +++ b/src/app/config_krux_installer.py @@ -0,0 +1,312 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +krux_installer.py +""" +import os +import sys +import json +import ctypes +import locale +from functools import partial +from kivy import resources as kv_resources +from kivy.clock import Clock +from kivy.core.text import LabelBase, DEFAULT_FONT +from src.utils.trigger import Trigger +from src.app.base_krux_installer import BaseKruxInstaller + + +class ConfigKruxInstaller(BaseKruxInstaller, Trigger): + """BaseKruxInstller is the base for Appliction""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # When program is frozen exe + # try to fix the problem with windows + # that do not render images on bundled .exe + # https://stackoverflow.com/questions/71656465/ + # how-to-include-image-in-kivy-application-with-onefile-mode-pyinstaller + # sys._MEIPASS is a temporary folder for PyInstaller. + if getattr(sys, "frozen", False): + # this is a Pyinstaller bundle + _meipass = getattr(sys, "_MEIPASS") + self.info(f"Adding resources from {_meipass}") + kv_resources.resource_add_path(_meipass) + self.assets_path = os.path.join(_meipass, "assets") + self.i18n_path = os.path.join(_meipass, "src", "i18n") + else: + cwd_path = os.path.dirname(__file__) + rel_assets_path = os.path.join(cwd_path, "..", "..", "assets") + rel_i18n_path = os.path.join(cwd_path, "..", "i18n") + self.assets_path = os.path.abspath(rel_assets_path) + self.i18n_path = os.path.abspath(rel_i18n_path) + + self.info(f"Registering assets path={self.assets_path}") + + noto_sans_path = os.path.join(self.assets_path, "NotoSansCJK_Cy_SC_KR_Krux.ttf") + LabelBase.register(DEFAULT_FONT, noto_sans_path) + + @staticmethod + def make_lang_code(lang: str) -> str: + """Properly say which language will be used based on system""" + if sys.platform in ("linux", "darwin"): + return f"{lang}.UTF-8" + + if sys.platform == "win32": + return lang + + raise OSError( + f"Couldn 't possible to setup locale: OS '{sys.platform}' not implemented" + ) + + @staticmethod + def get_system_lang(): + """Get operational system LANG""" + if sys.platform in ("linux", "darwin"): + return os.getenv("LANG") + + if sys.platform == "win32": + windll = ctypes.windll.kernel32 + return locale.windows_locale[windll.GetUserDefaultUILanguage()] + + raise OSError(f"OS '{sys.platform}' not recognized") + + @staticmethod + def get_app_dir(name: str) -> str | None: + """ "Get the full path of config folder""" + if name not in ("config", "local"): + raise ValueError(f"Invalid name: '{name}'") + + _system = sys.platform + + if _system in ("linux", "darwin"): + localappdata = os.path.expanduser("~") + return os.path.join(localappdata, f".{name}", "krux-installer") + + if _system == "win32": + if "LOCALAPPDATA" in os.environ: + localappdata = os.environ["LOCALAPPDATA"] + if localappdata is not None and localappdata != "": + return os.path.join(localappdata, "krux-installer", name) + + raise EnvironmentError("LOCALAPPDATA is empty") + + raise EnvironmentError("LOCALAPPDATA environment variable not found") + + raise OSError(f"Not supported: {sys.platform}") + + @staticmethod + def create_app_dir(name: str) -> str: + """ "Create the config folder""" + path = ConfigKruxInstaller.get_app_dir(name=name) + if not os.path.exists(path): + os.makedirs(path) + return path + + @staticmethod + def create_app_file(context: str, name: str) -> str: + """ "Create config.ini file""" + path = ConfigKruxInstaller.get_app_dir(name=context) + file = os.path.join(path, name) + if not os.path.exists(file): + with open(file, "w", encoding="utf8") as f: + f.write("# Generated config. Do not edit this manually!\n") + + return file + + # pylint: disable=signature-differs,arguments-differ + def get_application_config(self) -> str: + """Custom path for config.ini""" + dirname = ConfigKruxInstaller.create_app_dir(name="config") + self.debug(f"Application directory: {dirname}") + + file = ConfigKruxInstaller.create_app_file(context="config", name="config.ini") + self.debug(f"ConfigKruxInstaller.get_application_config = {file}") + + return super().get_application_config(file) + + def build_config(self, config): + """Create default configurations for app""" + _dir = ConfigKruxInstaller.create_app_dir(name="local") + + config.setdefaults("destdir", {"assets": _dir}) + self.debug(f"{config}.destdir={_dir}") + + baudrate = 1500000 + config.setdefaults("flash", {"baudrate": baudrate}) + self.debug(f"{config}.baudrate={baudrate}") + + lang = ConfigKruxInstaller.get_system_lang() + + # Check if system lang is supported in src/i18n + # and if not, defaults to en_US + if sys.platform in ("linux", "darwin"): + lang_file = os.path.join(self.i18n_path, f"{lang}.json") + if os.path.isfile(lang_file): + config.setdefaults("locale", {"lang": lang}) + self.info(f"{config}.lang={lang}") + + else: + self.warning(f"{lang} not supported. Default {config}.lang=en_US.UTF-8") + config.setdefaults("locale", {"lang": "en_US.UTF-8"}) + + if sys.platform == "win32": + lang_file = os.path.join(self.i18n_path, f"{lang}.UTF-8.json") + if os.path.isfile(lang_file): + config.setdefaults("locale", {"lang": lang}) + self.info(f"{config}.lang={lang}") + + else: + self.warning(f"{lang} not supported. Default {config}.lang=en_US") + config.setdefaults("locale", {"lang": "en_US"}) + + def build_settings(self, settings): + """Create settings panel""" + json_data = [ + { + "type": "path", + "title": "Assets's destination path", + "desc": "Destination path of downloaded assets", + "section": "destdir", + "key": "assets", + }, + { + "type": "numeric", + "title": "Flash baudrate", + "desc": "Applied baudrate during the flash process", + "section": "flash", + "key": "baudrate", + }, + { + "type": "options", + "title": "Locale", + "desc": "Application locale", + "section": "locale", + "key": "lang", + "options": [ + ConfigKruxInstaller.make_lang_code("af_ZA"), + ConfigKruxInstaller.make_lang_code("en_US"), + ConfigKruxInstaller.make_lang_code("es_ES"), + ConfigKruxInstaller.make_lang_code("fr_FR"), + ConfigKruxInstaller.make_lang_code("it_IT"), + ConfigKruxInstaller.make_lang_code("ko_KR"), + ConfigKruxInstaller.make_lang_code("nl_NL"), + ConfigKruxInstaller.make_lang_code("pt_BR"), + ConfigKruxInstaller.make_lang_code("ru_RU"), + ConfigKruxInstaller.make_lang_code("zh_CN"), + ], + }, + ] + + json_str = json.dumps(json_data) + self.debug(f"{settings}.data={json_str}") + settings.add_json_panel("Settings", self.config, data=json_str) + + def on_config_change(self, config, section, key, value): + if section == "locale" and key == "lang": + main = self.screen_manager.get_screen("MainScreen") + vers = self.screen_manager.get_screen("SelectVersionScreen") + oldv = self.screen_manager.get_screen("SelectOldVersionScreen") + warn_stable = self.screen_manager.get_screen( + "WarningAlreadyDownloadedScreen" + ) + warn_beta = self.screen_manager.get_screen("WarningBetaScreen") + verify = self.screen_manager.get_screen("VerifyStableZipScreen") + unzip = self.screen_manager.get_screen("UnzipStableScreen") + about = self.screen_manager.get_screen("AboutScreen") + + if sys.platform == "win32": + value = f"{value}.UTF-8" + + partials = [ + partial( + main.update, name="ConfigKruxInstaller", key="locale", value=value + ), + partial( + main.update, + name="ConfigKruxInstaller", + key="version", + value=main.version, + ), + partial( + main.update, + name="ConfigKruxInstaller", + key="device", + value=main.device, + ), + partial( + main.update, name="ConfigKruxInstaller", key="flash", value=None + ), + partial( + main.update, name="ConfigKruxInstaller", key="wipe", value=None + ), + partial( + main.update, name="ConfigKruxInstaller", key="settings", value=None + ), + partial( + main.update, name="ConfigKruxInstaller", key="about", value=None + ), + partial( + vers.update, name="ConfigKruxInstaller", key="locale", value=value + ), + partial( + oldv.update, name="ConfigKruxInstaller", key="locale", value=value + ), + partial( + warn_stable.update, + name="ConfigKruxInstaller", + key="locale", + value=value, + ), + partial( + warn_beta.update, + name="ConfigKruxInstaller", + key="locale", + value=value, + ), + partial( + verify.update, name="ConfigKruxInstaller", key="locale", value=value + ), + partial( + unzip.update, name="ConfigKruxInstaller", key="locale", value=value + ), + partial( + about.update, name="ConfigKruxInstaller", key="locale", value=value + ), + ] + + if sys.platform == "linux": + check = self.screen_manager.get_screen("CheckPermissionsScreen") + partials.append( + partial( + check.update, + name="ConfigKruxInstaller", + key="locale", + value=value, + ) + ) + + for fn in partials: + Clock.schedule_once(fn, 0) + + else: + self.debug(f"Skip on_config_change for {section}::{key}={value}") diff --git a/src/app/screens/about_screen.py b/src/app/screens/about_screen.py new file mode 100644 index 00000000..5399e750 --- /dev/null +++ b/src/app/screens/about_screen.py @@ -0,0 +1,133 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +about_screen.py +""" +from functools import partial +import webbrowser +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.graphics.vertex_instructions import Rectangle +from kivy.graphics.context_instructions import Color +from src.utils.constants import get_name, get_version +from src.app.screens.base_screen import BaseScreen + + +class AboutScreen(BaseScreen): + """Flash screen is where flash occurs""" + + def __init__(self, **kwargs): + super().__init__(wid="about_screen", name="AboutScreen", **kwargs) + self.src_code = ( + "https://selfcustody.github.io/krux/getting-started/installing/from-gui/" + ) + + self.make_grid(wid="about_screen_grid", rows=1) + + self.make_label( + wid=f"{self.id}_label", + text="", + root_widget=f"{self.id}_grid", + markup=True, + halign="justify", + ) + + def _on_ref_press(*args): + self.debug(f"Calling Button::{args[0]}::on_ref_press") + self.debug(f"Opening {args[1]}") + + if args[1] == "Back": + self.set_screen(name="MainScreen", direction="right") + + elif args[1] == "X": + webbrowser.open("https://x.com/selfcustodykrux") + + elif args[1] == "SourceCode": + webbrowser.open(self.src_code) + + else: + self.redirect_error(f"Invalid ref: {args[1]}") + + setattr(self, f"on_ref_press_{self.id}_label", _on_ref_press) + self.ids[f"{self.id}_label"].bind(on_ref_press=_on_ref_press) + + fns = [ + partial(self.update, name=self.name, key="canvas"), + partial(self.update, name=self.name, key="locale", value=self.locale), + ] + + for fn in fns: + Clock.schedule_once(fn, 0) + + def update(self, *args, **kwargs): + """Update buttons from selected device/versions on related screens""" + name = kwargs.get("name") + key = kwargs.get("key") + value = kwargs.get("value") + + # Check if update to screen + if name in ("KruxInstallerApp", "ConfigKruxInstaller", "AboutScreen"): + self.debug(f"Updating {self.name} from {name}...") + else: + self.redirect_error(msg=f"Invalid screen name: {name}") + + if key == "canvas": + # prepare background + with self.canvas.before: + Color(0, 0, 0, 1) + Rectangle(size=(Window.width, Window.height)) + + # Check locale + elif key == "locale": + if value is not None: + self.locale = value + follow = self.translate("follow us on X") + back = self.translate("Back") + + self.ids[f"{self.id}_label"].text = "".join( + [ + f"[size={self.SIZE_G}sp]", + f"[ref=SourceCode][b]v{get_version()}[/b][/ref]", + "[/size]", + "\n", + "\n" f"[size={self.SIZE_M}sp]", + f"{follow}: ", + "[color=#00AABB]", + "[ref=X][u]@selfcustodykrux[/u][/ref]", + "[/color]", + "[/size]", + "\n", + "\n", + f"[size={self.SIZE_M}sp]", + "[color=#00FF00]", + "[ref=Back]", + f"[u]{back}[/u]", + "[/ref]", + "[/color]", + "[/size]", + ] + ) + + else: + self.redirect_error(f"Invalid value for key '{key}': '{value}'") + + else: + self.redirect_error(msg=f'Invalid key: "{key}"') diff --git a/src/app/screens/base_download_screen.py b/src/app/screens/base_download_screen.py new file mode 100644 index 00000000..06d1e553 --- /dev/null +++ b/src/app/screens/base_download_screen.py @@ -0,0 +1,165 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +base_download_screen.py +""" +import typing +from functools import partial +from threading import Thread +from kivy.clock import Clock, ClockEvent +from kivy.weakproxy import WeakProxy +from kivy.uix.label import Label +from src.app.screens.base_screen import BaseScreen +from src.utils.downloader.asset_downloader import AssetDownloader + + +class BaseDownloadScreen(BaseScreen): + """BaseDownloadScreen setup some initial variables for usable Downloader screens""" + + def __init__(self, wid: str, name: str, **kwargs): + super().__init__(wid=wid, name=name, **kwargs) + self.make_grid(wid=f"{self.id}_grid", rows=2) + + self._downloader = None + self._thread = None + self._trigger = None + self.version = None + self._to_screen = "" + + # progress label, show a "Connecting" + # before start the download to make + connecting = self.translate("Connecting") + text = "".join( + [ + f"[size={self.SIZE_G}]", + f"{connecting}...", + "[/size]", + "[color=#efcc00]", + "[/color]", + ] + ) + + progress = Label( + text=text, + markup=True, + valign="center", + halign="center", + ) + + # information label + # it has data about url + # and downloaded paths + asset_label = Label(markup=True, valign="center", halign="center") + + # setup progress label + progress.id = f"{self.id}_progress" + self.ids[f"{self.id}_grid"].add_widget(progress) + self.ids[progress.id] = WeakProxy(progress) + + # setup information label + asset_label.id = f"{self.id}_info" + self.ids[f"{self.id}_grid"].add_widget(asset_label) + self.ids[asset_label.id] = WeakProxy(asset_label) + + @property + def to_screen(self) -> str: + self.debug(f"getter::to_screen={self._to_screen}") + return self._to_screen + + @to_screen.setter + def to_screen(self, value: str): + self.debug(f"setter::to_screen={value}") + self._to_screen = value + + @property + def downloader(self) -> AssetDownloader | None: + """Get an `AssetDownloader`""" + self.debug(f"getter::downloader={self._downloader}") + return self._downloader + + @downloader.setter + def downloader(self, value: AssetDownloader): + """Set an `AssetDownloader`""" + self.debug(f"setter::downloader={value}") + self._downloader = value + + @downloader.deleter + def downloader(self): + """Delete an `AssetDownloader`""" + self.debug(f"deleter::downloader={self._downloader}") + del self._downloader + + @property + def thread(self) -> Thread | None: + self.debug(f"getter::thread={self._thread}") + return self._thread + + @thread.setter + def thread(self, value: Thread): + """ + Wait until download thread finish, + when finished call this callback + + See https://kivy.org/doc/stable/guide/events.html + """ + self.debug(f"setter::thread={self._thread}->{value}") + self._thread = value + + @property + def trigger(self) -> ClockEvent: + """Trigger is a `ClockEvent` that should be triggered after download is done""" + self.debug(f"getter::trigger={self._trigger}") + return self._trigger + + @trigger.setter + def trigger(self, value: typing.Callable): + """Create a `ClockEvent` given a callback""" + self.debug(f"getter::trigger={value}") + self._trigger = Clock.create_trigger(value) + + @trigger.deleter + def trigger(self): + """Delete a `ClockEvent`""" + self.debug(f"deleter::trigger={self._trigger}") + del self._trigger + + def on_enter(self): + """ + Event fired when the screen is displayed and the entering animation is complete. + + Every inherithed class should implement it own `on_trigger` and `on_progress` + staticmethods. The method `on_progress` should call `self.trigger` ath the end: + """ + if self.downloader is not None: + # on trigger should be defined on inherited classes + self.trigger = getattr(self.__class__, "on_trigger") + + # on progress should be defined on inherited classes + on_progress = getattr(self.__class__, "on_progress") + _fn = partial(self.downloader.download, on_data=on_progress) + + # Now run it as a partial function + # on parallel thread to not block + # the process during the kivy cycles + self.thread = Thread(name=self.name, target=_fn) + self.thread.start() + else: + self.redirect_error("Downloader isnt configured. Use `update` method first") diff --git a/src/app/screens/base_flash_screen.py b/src/app/screens/base_flash_screen.py new file mode 100644 index 00000000..ff2d6363 --- /dev/null +++ b/src/app/screens/base_flash_screen.py @@ -0,0 +1,115 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +main_screen.py +""" +import os +import math +import typing +from pathlib import Path +from functools import partial +from threading import Thread +from kivy.clock import Clock, ClockEvent +from src.utils.flasher import Flasher +from src.app.screens.base_screen import BaseScreen + + +class BaseFlashScreen(BaseScreen): + """Flash screen is where flash occurs""" + + def __init__(self, wid: str, name: str, **kwargs): + super().__init__(wid=wid, name=name, **kwargs) + self.make_grid(wid=f"{self.id}_grid", rows=2) + self._firmware = None + self._baudrate = None + self._thread = None + self._trigger = None + self._output = None + self._progress = None + self._is_done = False + + @property + def firmware(self) -> str: + """Getter for firmware""" + self.debug(f"getter::firmware={self._firmware}") + return self._firmware + + @firmware.setter + def firmware(self, value: str): + """Setter for firmware""" + if os.path.exists(value): + self.debug(f"setter::firmware={value}") + self._firmware = value + else: + raise ValueError(f"Firmware not exist: {value}") + + @property + def baudrate(self) -> str: + """Getter for baudrate""" + self.debug(f"getter::baudrate={self._baudrate}") + return self._baudrate + + @baudrate.setter + def baudrate(self, value: str): + """Setter for baudrate""" + self.debug(f"setter::baudrate={value}") + self._baudrate = value + + @property + def thread(self) -> Thread: + """Getter for thread""" + self.debug(f"getter::thread={self._thread}") + return self._thread + + @thread.setter + def thread(self, value: Thread): + """ + Wait until download thread finish, + when finished call this callback + + See https://kivy.org/doc/stable/guide/events.html + """ + self.debug(f"setter::thread={self._thread}->{value}") + self._thread = value + + @property + def trigger(self) -> ClockEvent: + """Trigger is a `ClockEvent` that should be triggered after download is done""" + self.debug(f"getter::trigger={self._thread}") + return self._trigger + + @trigger.setter + def trigger(self, value: typing.Callable): + """Create a `ClockEvent` given a callback""" + self.debug(f"getter::trigger={self._trigger}") + self._trigger = Clock.create_trigger(value) + + @property + def output(self) -> typing.List[str]: + """Getter for output""" + self.debug(f"getter::output={self._output}") + return self._output + + @output.setter + def output(self, value: typing.List[str]): + """Setter for info""" + self.debug(f"setter::output={value}") + self._output = value diff --git a/src/app/screens/base_screen.py b/src/app/screens/base_screen.py new file mode 100644 index 00000000..ecaa7b46 --- /dev/null +++ b/src/app/screens/base_screen.py @@ -0,0 +1,303 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +base_screen.py +""" +import os +import re +import sys +import typing +from pathlib import Path +from functools import partial +from kivy.clock import Clock +from kivy.app import App +from kivy.core.window import Window +from kivy.uix.label import Label +from kivy.uix.button import Button +from kivy.uix.gridlayout import GridLayout +from kivy.uix.image import Image +from kivy.core.window import Window +from kivy.weakproxy import WeakProxy +from kivy.uix.screenmanager import Screen +from src.utils.trigger import Trigger +from src.i18n import T +from src.utils.selector import VALID_DEVICES + + +class BaseScreen(Screen, Trigger): + """Main screen is the 'Home' page""" + + def __init__(self, wid: str, name: str, **kwargs): + super().__init__(**kwargs) + self.id = wid + self.name = name + + # Check if this is a Pyinstaller bundle + # and set the correct path to find some assets + if getattr(sys, "frozen", False): + root_assets_path = getattr(sys, "_MEIPASS") + else: + root_assets_path = Path(__file__).parent.parent.parent.parent + + self._logo_img = os.path.join(root_assets_path, "assets", "logo.png") + self._warn_img = os.path.join(root_assets_path, "assets", "warning.png") + self._load_img = os.path.join(root_assets_path, "assets", "load.gif") + self._done_img = os.path.join(root_assets_path, "assets", "done.png") + self._error_img = os.path.join(root_assets_path, "assets", "error.png") + + self.locale = BaseScreen.get_locale() + + # Setup the correct font size + if sys.platform in ("linux", "win32"): + self.SIZE_XG = Window.size[0] // 4 + self.SIZE_GG = Window.size[0] // 8 + self.SIZE_G = Window.size[0] // 16 + self.SIZE_MM = Window.size[0] // 24 + self.SIZE_M = Window.size[0] // 32 + self.SIZE_MP = Window.size[0] // 48 + self.SIZE_P = Window.size[0] // 64 + self.SIZE_PP = Window.size[0] // 128 + + elif sys.platform == "darwin": + self.SIZE_XG = Window.size[0] // 16 + self.SIZE_GG = Window.size[0] // 24 + self.SIZE_G = Window.size[0] // 32 + self.SIZE_MM = Window.size[0] // 48 + self.SIZE_M = Window.size[0] // 64 + self.SIZE_MP = Window.size[0] // 128 + self.SIZE_P = Window.size[0] // 192 + self.SIZE_PP = Window.size[0] // 256 + + @property + def logo_img(self) -> str: + """Getter for logo_img""" + self.debug(f"getter::logo_img={self._logo_img}") + return self._logo_img + + @property + def warn_img(self) -> str: + """Getter for warn_img""" + self.debug(f"getter::warn_img={self._warn_img}") + return self._warn_img + + @property + def load_img(self) -> str: + """Getter for load_img""" + self.debug(f"getter::load_img={self._load_img}") + return self._load_img + + @property + def done_img(self) -> str: + """Getter for done_img""" + self.debug(f"getter::done_img={self._done_img}") + return self._done_img + + @property + def error_img(self) -> str: + """Getter for logo_img""" + self.debug(f"getter::error_img={self._logo_img}") + return self._error_img + + @property + def locale(self) -> str: + """Getter for locale property""" + return self._locale + + @locale.setter + def locale(self, value: bool): + """Setter for locale property""" + self.debug(f"locale = {value}") + self._locale = value + + def translate(self, key: str) -> str: + msg = T(key, locale=self.locale, module=self.id) + self.debug(f"Translated '{key}' to '{msg}'") + return msg + + def set_background(self, wid: str, rgba: typing.Tuple[float, float, float, float]): + """Changes the widget's background by it's id""" + widget = self.ids[wid] + msg = f"Button::{wid}.background_color={rgba}" + self.debug(msg) + widget.background_color = rgba + + def set_screen(self, name: str, direction: typing.Literal["left", "right"]): + """Change to some screen registered on screen_manager""" + msg = f"Switching to screen='{name}' by direction='{direction}'" + self.debug(msg) + self.manager.transition.direction = direction + self.manager.current = name + + def make_grid(self, wid: str, rows: int): + """Build grid where buttons will be placed""" + if wid not in self.ids: + self.debug(f"Building GridLayout::{wid}") + grid = GridLayout(cols=1, rows=rows) + grid.id = wid + self.add_widget(grid) + self.ids[wid] = WeakProxy(grid) + else: + self.debug(f"GridLayout::{wid} already exist") + + def make_subgrid(self, wid: str, rows: int, root_widget: str): + """Build grid where buttons will be placed""" + self.debug(f"Building GridLayout::{wid}") + grid = GridLayout(cols=1, rows=rows) + grid.id = wid + self.ids[root_widget].add_widget(grid) + self.ids[wid] = WeakProxy(grid) + + def make_label( + self, wid: str, text: str, root_widget: str, markup: bool, halign: str + ): + """Build grid where buttons will be placed""" + self.debug(f"Building GridLayout::{wid}") + label = Label(text=text, markup=markup, halign=halign) + label.id = wid + self.ids[root_widget].add_widget(label) + self.ids[wid] = WeakProxy(label) + + def make_image(self, wid: str, source: str, root_widget: str): + """Build grid where buttons will be placed""" + self.debug(f"Building Image::{wid}") + image = Image(source=source, fit_mode="scale-down") + image.id = wid + self.ids[root_widget].add_widget(image) + self.ids[wid] = WeakProxy(image) + + def clear_grid(self, wid: str): + """Clear GridLayout widget""" + self.debug(f"Clearing widgets from GridLayout::{wid}") + self.ids[wid].clear_widgets() + + def make_button( + self, + root_widget: str, + id: str, + text: str, + markup: bool, + row: int, + on_press: typing.Callable, + on_release: typing.Callable, + ): + """Create buttons in a dynamic way""" + self.debug(f"{id} -> {root_widget}") + + total = self.ids[root_widget].rows + btn = Button( + text=text, + markup=markup, + halign="center", + font_size=Window.size[0] // 25, + background_color=(0, 0, 0, 1), + color=(1, 1, 1, 1), + ) + btn.id = id + + # define button methods to be callable in classes + setattr(self, f"on_press_{id}", on_press) + setattr(self, f"on_release_{id}", on_release) + + btn.bind(on_press=on_press) + btn.bind(on_release=on_release) + btn.x = 0 + btn.y = (Window.size[1] / total) * row + btn.width = Window.size[0] + btn.height = Window.size[1] / total + self.ids[root_widget].add_widget(btn) + self.ids[btn.id] = WeakProxy(btn) + + self.debug( + f"button::{id} row={row}, pos_hint={btn.pos_hint}, size_hint={btn.size_hint}" + ) + + def make_stack_button( + self, + root_widget: str, + wid: str, + on_press: typing.Callable, + on_release: typing.Callable, + size_hint: typing.Tuple[float, float], + ): + btn = Button( + markup=True, + font_size=Window.size[0] // 30, + background_color=(0, 0, 0, 1), + size_hint=size_hint, + ) + btn.id = wid + self.ids[root_widget].add_widget(btn) + self.ids[btn.id] = WeakProxy(btn) + btn.bind(on_press=on_press) + btn.bind(on_release=on_release) + setattr(self, f"on_press_{wid}", on_press) + setattr(self, f"on_release_{wid}", on_release) + + def redirect_error(self, msg: str): + exception = RuntimeError(msg) + self.redirect_exception(exception=exception) + + def redirect_exception(self, exception: Exception): + screen = self.manager.get_screen("ErrorScreen") + fns = [ + partial(screen.update, name=self.name, key="error", value=exception), + partial(screen.update, name=self.name, key="canvas"), + ] + + for fn in fns: + Clock.schedule_once(fn, 0) + + self.set_screen(name="ErrorScreen", direction="left") + + @staticmethod + def get_destdir_assets() -> str: + app = App.get_running_app() + return app.config.get("destdir", "assets") + + @staticmethod + def get_baudrate() -> int: + app = App.get_running_app() + return int(app.config.get("flash", "baudrate")) + + @staticmethod + def get_locale() -> str: + app = App.get_running_app() + locale = app.config.get("locale", "lang") + + if sys.platform in ("linux", "darwin"): + locale = locale.split(".") + return f"{locale[0].replace("-", "_")}.{locale[1]}" + + elif sys.platform == "win32": + return f"{locale}.UTF-8" + + else: + raise RuntimeError(f"Not implemented for '{sys.platform}'") + + @staticmethod + def open_settings(): + app = App.get_running_app() + app.open_settings() + + @staticmethod + def sanitize_markup(msg: str) -> str: + cleanr = re.compile("\\[.*?\\]") + return re.sub(cleanr, "", msg) diff --git a/src/app/screens/check_internet_connection_screen.py b/src/app/screens/check_internet_connection_screen.py new file mode 100644 index 00000000..5f6f7d5d --- /dev/null +++ b/src/app/screens/check_internet_connection_screen.py @@ -0,0 +1,130 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +check_internet_connection_screen.py +""" +from functools import partial +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.graphics.vertex_instructions import Rectangle +from kivy.graphics.context_instructions import Color +from kivy.app import App +from kivy.cache import Cache +from src.app.screens.base_screen import BaseScreen +from src.utils.selector import Selector + + +class CheckInternetConnectionScreen(BaseScreen): + """ + CheckInternetConnectionScreen will check internet connection and get the + latest release if ok + """ + + def __init__(self, **kwargs): + super().__init__( + wid="check_internet_connection_screen", + name="CheckInternetConnectionScreen", + **kwargs, + ) + + # Build grid where buttons will be placed + self.make_grid(wid=f"{self.id}_grid", rows=1) + + # START of on_press buttons + def _press(instance): + self.debug(f"Calling Button::{instance.id}::on_press") + self.set_background(wid=instance.id, rgba=(0.25, 0.25, 0.25, 1)) + + def _release(instance): + self.debug(f"Calling Button::{instance.id}::on_release") + self.set_background(wid=f"{instance.id}", rgba=(0, 0, 0, 1)) + + self.make_button( + row=0, + id=f"{self.id}_button", + root_widget=f"{self.id}_grid", + text="".join( + [ + f"[size={self.SIZE_MM}sp]", + "[color=#efcc00]", + self.translate("Checking your internet connection"), + "[/color]", + "[/size]", + ] + ), + markup=True, + on_press=_press, + on_release=_release, + ) + + def update(self, *args, **kwargs): + """ + In linux, will check for user permission on group + dialout (debian-li ke) and uucp (archlinux-like) and + add user to that group to allow sudoless flash + """ + name = kwargs.get("name") + key = kwargs.get("key") + value = kwargs.get("value") + + if name in ("ConfigKruxInstaller", "CheckInternetConnectionScreen"): + self.debug(f"Updating {self.name} from {name}...") + else: + self.redirect_error(f"Invalid screen: {name}") + + if key == "locale": + # Setup + self.locale = value + + elif key == "canvas": + # prepare background + with self.canvas.before: + Color(0, 0, 0, 1) + Rectangle(size=(Window.width, Window.height)) + + elif key == "check-connection": + try: + selector = Selector() + main_screen = self.manager.get_screen("MainScreen") + fn = partial( + main_screen.update, + name="KruxInstallerApp", + key="version", + value=selector.releases[0], + ) + Clock.schedule_once(fn, 0) + self.set_screen(name="MainScreen", direction="left") + + except Exception as exc: + self.redirect_exception(exception=exc) + + else: + self.redirect_error(f"Invalid key: '{key}'") + + def on_enter(self): + """Simple update your canvas""" + partials = [ + partial(self.update, name=self.name, key="canvas"), + partial(self.update, name=self.name, key="check-connection"), + ] + + for fn in partials: + Clock.schedule_once(fn, 0) diff --git a/src/app/screens/check_permissions_screen.py b/src/app/screens/check_permissions_screen.py new file mode 100644 index 00000000..5ec3825e --- /dev/null +++ b/src/app/screens/check_permissions_screen.py @@ -0,0 +1,313 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +check_permissions_screen.py +""" +import os +import re +import typing +import sys +import time +from functools import partial +from kivy.clock import Clock +from kivy.app import App +from kivy.graphics.context_instructions import Color +from kivy.graphics.vertex_instructions import Rectangle +from kivy.core.window import Window +from .base_screen import BaseScreen +from src.i18n import T +from pysudoer import SudoerLinux + +if not sys.platform.startswith("win"): + import distro + import grp + + +class CheckPermissionsScreen(BaseScreen): + """GreetingsScreen show Krux logo and check if user is in dialout group in linux""" + + def __init__(self, **kwargs): + super().__init__( + wid="check_permissions_screen", + name="CheckPermissionsScreen", + **kwargs, + ) + + # Build grid where buttons will be placed + self.make_grid(wid=f"{self.id}_grid", rows=1) + + # These variables will setup the inclusion + # in dialout group, if necessary + self.bin = None + self.bin_args = None + self.group = None + self.user = None + self.in_dialout = False + self.on_permission_created = None + + def _on_ref_press(*args): + print(args) + if args[1] == "Allow": + # If user isnt in the dialout group, + # but the configuration was done correctly + # create the command + + if self.on_permission_created and self.bin_args: + try: + cmd = ( + [self.bin] + + [a for a in self.bin_args] + + [self.group] + + [self.user] + ) + self.debug(f"cmd={cmd}") + sudoer = SudoerLinux(name=f"Add {self.user} to {self.group}") + sudoer.exec( + cmd=cmd, env={}, callback=self.on_permission_created + ) + except Exception as err: + self.redirect_error(msg=str(err.__traceback__)) + else: + self.redirect_error( + msg=f"Invalid on_permission_created: {self.on_permission_created}" + ) + + if args[1] == "Deny": + App.get_running_app().stop() + + self.make_label( + wid=f"{self.id}_label", + text="", + root_widget=f"{self.id}_grid", + markup=True, + halign="justify", + ) + + setattr(CheckPermissionsScreen, f"on_ref_press_{self.id}_label", _on_ref_press) + self.ids[f"{self.id}_label"].bind(on_ref_press=_on_ref_press) + + fn = partial(self.update, name=self.name, key="canvas") + Clock.schedule_once(fn, 0) + + def update(self, *args, **kwargs): + """ + In linux, will check for user permission on group + dialout (debian-li ke) and uucp (archlinux-like) and + add user to that group to allow sudoless flash + """ + name = kwargs.get("name") + key = kwargs.get("key") + value = kwargs.get("value") + + if name in ( + "ConfigKruxInstaller", + "CheckPermissionsScreen", + "CheckPermissionsScreen", + ): + self.debug(f"Updating {self.name} from {name}...") + else: + self.redirect_error(msg=f"Invalid screen: '{name}'") + return + + if key == "locale": + if value is None or value.strip() == "": + self.redirect_error(msg=f"Invalid locale: '{value}'") + else: + self.locale = value + + elif key == "canvas": + # prepare background + with self.canvas.before: + Color(0, 0, 0, 1) + Rectangle(size=(Window.width, Window.height)) + + elif key == "check_user": + + # Here's where the check process start + # first get the current user, then verify + # the linux distribution used and use the + # proper command to add the user in 'dialout' ( + # group (in some distros, can be 'uucp') + self.user = os.environ.get("USER") + self.debug(f"Checking permissions for {self.user}") + + setup_msg = self.translate("Setup") + for_msg = self.translate("for") + + self.ids[f"{self.id}_label"].text = "".join( + [ + f"[size={self.SIZE_MM}sp]", + "[color=#efcc00]", + f"{setup_msg} {self.user} {for_msg} {distro.name()}", + "[/color]", + "[/size]", + ] + ) + + if distro.id() in ("ubuntu", "fedora", "linuxmint"): + self.bin = "/usr/bin/usermod" + self.bin_arg = ["-a", "-G"] + self.group = "dialout" + + elif distro.id() in ("arch", "manjaro", "slackware", "gentoo"): + self.bin = "/usr/bin/usermod" + self.bin_args = ["-a", "-G"] + self.group = "uucp" + + elif distro.like() == "debian": + self.bin = "/usr/bin/usermod" + self.bin_args = ["-a", "-G"] + self.group = "dialout" + + else: + self.redirect_error( + msg=f"Not implemented for '{distro.name(pretty=True)}'" + ) + return + + fn = partial(self.update, name=self.name, key="check_group") + Clock.schedule_once(fn, 2.1) + + elif key == "check_group": + + # Here is where we check if the user belongs to group + # 'dialout' (in some distros, will be 'uucp') + self.debug(f"Checking {self.group} permissions for {self.user}") + check_msg = self.translate("Checking") + perm_msg = self.translate("permissions for") + + self.ids[f"{self.id}_label"].text = "".join( + [ + f"[size={self.SIZE_G}sp]", + "[color=#efcc00]", + f"{check_msg} {self.group} {perm_msg} {self.user}", + "[/color]", + "[/size]", + ] + ) + + # loop throug all groups and check + for group in grp.getgrall(): + if self.group == group.gr_name: + self.debug(f"Found {group.gr_name}") + for user in group[3]: + if user == self.user: + self.debug(f"'{self.user}' already in group '{self.group}'") + self.in_dialout = True + + self.debug(f"in_dialout={self.in_dialout}") + + # if not in group, warn user + # and then ask for click in screen + # to proceed with the operation + if not self.in_dialout: + self.debug(f"Creating permission for {self.user}") + warn_msg = self.translate("WARNING") + first_msg = self.translate("This is the first run of KruxInstaller in") + access_msg = self.translate( + "and it appears that you do not have privileged access to make flash procedures" + ) + proceed_msg = self.translate( + "To proceed, click in the Allow button and a prompt will ask for your password" + ) + exec_msg = self.translate("to execute the following command") + + self.ids[f"{self.id}_label"].text = "\n".join( + [ + f"[size={self.SIZE_G}sp][color=#efcc00]{warn_msg}[/color][/size]", + "", + f'[size={self.SIZE_MP}sp]{first_msg} "{distro.name(pretty=True)}"', + f"{access_msg}.", + proceed_msg, + f"{exec_msg}:", + "", + "[color=#00ff00]", + f"{self.bin} {" ".join(self.bin_args or [])} {self.group} {self.user}", + "[/color]", + "[/size]", + "", + "", + f"[size={self.SIZE_M}]", + " ".join( + [ + "[color=#00FF00][ref=Allow]Allow[/ref][/color]", + "[color=#FF0000][ref=Deny]Deny[/ref][/color]", + ] + ), + "[/size]", + ] + ) + + # Check if callback is created and create if isnt exist + # (in tests you can mock it and the conditional below will not be called) + if self.on_permission_created is None: + fn = partial( + self.update, name=self.name, key="make_on_permission_created" + ) + Clock.schedule_once(fn, 2.1) + + else: + self.set_screen(name="CheckInternetConnectionScreen", direction="left") + + elif key == "make_on_permission_created": + + # When user is added to dialout group + # ask for user to reboot to apply the changes + # and the ability to flash take place + def on_permission_created(output: str): + self.debug(f"output={output}") + logout_msg = self.translate("You may need to logout (or even reboot)") + backin_msg = self.translate( + "and back in for the new group to take effect" + ) + not_worry_msg = self.translate( + "Do not worry, this message won't appear again" + ) + + self.ids[f"{self.id}_label"].text = "\n".join( + [ + f"[size={self.SIZE_M}sp][color=#efcc00]{output}[/color][/size]", + "", + f"[size={self.SIZE_M}sp]{logout_msg}", + f"{backin_msg}.", + "", + f"{not_worry_msg}.[/size]", + ] + ) + + self.bin = None + self.bin_args = None + self.group = None + self.user = None + + setattr(self, "on_permission_created", on_permission_created) + + else: + self.redirect_error(msg=f"Invalid key: '{key}'") + + def on_enter(self): + """ + check if user belongs to dialout|uucp group + (groups that manage /tty/USB files) + if belongs, add user to it + """ + fn = partial(self.update, name=self.name, key="check_user") + Clock.schedule_once(fn, 0) diff --git a/src/app/screens/download_beta_screen.py b/src/app/screens/download_beta_screen.py new file mode 100644 index 00000000..9e7b123d --- /dev/null +++ b/src/app/screens/download_beta_screen.py @@ -0,0 +1,213 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +main_screen.py +""" +import os +import time +from functools import partial +from kivy.app import App +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.graphics.vertex_instructions import Rectangle +from kivy.graphics.context_instructions import Color +from src.app.screens.base_download_screen import BaseDownloadScreen +from src.utils.downloader.beta_downloader import BetaDownloader + + +class DownloadBetaScreen(BaseDownloadScreen): + """DownloadBetaScreen manage the download process of beta releases""" + + def __init__(self, **kwargs): + super().__init__( + wid="download_beta_screen", name="DownloadBetaScreen", **kwargs + ) + self.to_screen = "FlashScreen" + self.firmware = None + self.device = None + + # Define some staticmethods in dynamic way + # (so they can be called in tests) + def on_trigger(dt): + time.sleep(2.1) + screen = self.manager.get_screen(self.to_screen) + baudrate = DownloadBetaScreen.get_baudrate() + destdir = DownloadBetaScreen.get_destdir_assets() + firmware = os.path.join( + destdir, "krux_binaries", f"maixpy_{self.device}", self.firmware + ) + partials = [ + partial(screen.update, name=self.name, key="baudrate", value=baudrate), + partial(screen.update, name=self.name, key="firmware", value=firmware), + partial(screen.update, name=self.name, key="flasher"), + ] + + for fn in partials: + Clock.schedule_once(fn, 0) + + self.set_screen(name=self.to_screen, direction="left") + + def on_progress(data: bytes): + # calculate downloaded percentage + if self.downloader is not None: + fn = partial( + self.update, + name=self.name, + key="progress", + value={ + "downloaded_len": self.downloader.downloaded_len, + "content_len": self.downloader.content_len, + }, + ) + Clock.schedule_once(fn, 0) + + else: + self.redirect_error("Downloader isnt initialized") + + self.debug(f"Bind {self.__class__}.on_trigger={on_trigger}") + setattr(self.__class__, "on_trigger", on_trigger) + + self.debug(f"Bind {self.__class__}.on_progress={on_progress}") + setattr(self.__class__, "on_progress", on_progress) + + fn = partial(self.update, name=self.name, key="canvas") + Clock.schedule_once(fn, 0) + + def update(self, *args, **kwargs): + """Update screen with version key. Should be called before `on_enter`""" + name = kwargs.get("name") + key = kwargs.get("key") + value = kwargs.get("value") + + if name in ("ConfigKruxInstaller", "MainScreen", "DownloadBetaScreen"): + self.debug(f"Updating {self.name} from {name}::{key}={value}") + else: + self.redirect_error(f"Invalid screen name: {name}") + return + + if key == "locale": + if value is not None: + self.locale = value + else: + self.redirect_error(f"Invalid value for key '{key}': '{value}'") + + elif key == "canvas": + # prepare background + with self.canvas.before: + Color(0, 0, 0, 1) + Rectangle(size=(Window.width, Window.height)) + + elif key == "firmware": + if value is None: + self.redirect_error(f"Invalid value for key '{key}': '{value}'") + elif value in ("kboot.kfpkg", "firmware.bin"): + self.firmware = value + else: + self.redirect_error(f"Invalid firmware: {value}") + + elif key == "device": + if value in ( + "m5stickv", + "amigo", + "dock", + "bit", + "yahboom", + "cube", + "wonder_mv", + ): + self.device = value + else: + self.redirect_error(f'Invalid device: "{value}"') + + elif key == "downloader": + + if self.downloader is None: + destdir = DownloadBetaScreen.get_destdir_assets() + destdir = os.path.join( + destdir, "krux_binaries", f"maixpy_{self.device}" + ) + + self.downloader = BetaDownloader( + device=self.device, + binary_type=self.firmware, + destdir=destdir, + ) + + downloading = self.translate("Downloading") + to = self.translate("to") + + self.ids[f"{self.id}_info"].text = "".join( + [ + f"[size={self.SIZE_MP}sp]", + downloading, + "\n", + "[color=#00AABB]", + f"[ref={self.downloader.url}]{self.downloader.url}[/ref]", + "[/color]", + "\n", + to, + "\n", + self.downloader.destdir, + "\n", + "[/size]", + ] + ) + + else: + self.redirect_error("Downloader already initialized") + + elif key == "progress": + # calculate percentage of download + if value is not None and self.downloader is not None: + lens = [value["downloaded_len"], value["content_len"]] + percent = lens[0] / lens[1] + + # Format bytes (one liner) in MB + # https://stackoverflow.com/questions/ + # 5194057/better-way-to-convert-file-sizes-in-python#answer-52684562 + downs = [f"{lens[0]/(1<<20):,.2f}", f"{lens[1]/(1<<20):,.2f}"] + self.ids[f"{self.id}_progress"].text = "".join( + [ + f"[size={self.SIZE_G}sp][b]{ percent * 100:,.2f} %[/b][/size]", + "\n", + f"[size={self.SIZE_MP}sp]{downs[0]} of {downs[1]} MB[/size]", + ] + ) + + if percent == 1.0: + downloaded = self.translate("downloaded") + destdir = os.path.join(self.downloader.destdir, "kboot.kfpkg") + self.ids[f"{self.id}_info"].text = "".join( + [ + f"[size={self.SIZE_MP}sp]", + destdir, + "\n", + downloaded, + "[/size]", + ] + ) + + # When finish, change the label, wait some seconds + # and then change screen + self.trigger() + + else: + self.redirect_error(f'Invalid key: "{key}"') diff --git a/src/app/screens/download_selfcustody_pem_screen.py b/src/app/screens/download_selfcustody_pem_screen.py new file mode 100644 index 00000000..0415eb5f --- /dev/null +++ b/src/app/screens/download_selfcustody_pem_screen.py @@ -0,0 +1,181 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +download_selfcustody_pem_screen.py +""" +import os +import time +from functools import partial +from kivy.app import App +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.graphics.vertex_instructions import Rectangle +from kivy.graphics.context_instructions import Color +from src.app.screens.base_screen import BaseScreen +from src.app.screens.base_download_screen import BaseDownloadScreen +from src.utils.downloader.pem_downloader import PemDownloader + + +class DownloadSelfcustodyPemScreen(BaseDownloadScreen): + """DownloadSelfcustodyPemScreen download the selfcustody's public key certificate""" + + def __init__(self, **kwargs): + super().__init__( + wid="download_selfcustody_pem_screen", + name="DownloadSelfcustodyPemScreen", + **kwargs, + ) + + self.to_screen = "VerifyStableZipScreen" + + # Define some staticmethods in dynamic way + # (so they can be called in tests) + def on_trigger(dt): + time.sleep(2.1) + self.set_screen(name=self.to_screen, direction="left") + + def on_progress(data: bytes): + # calculate downloaded percentage + fn = partial( + self.update, + name=self.name, + key="progress", + value={ + "downloaded_len": self.downloader.downloaded_len, + "content_len": self.downloader.content_len, + }, + ) + Clock.schedule_once(fn, 0) + + self.debug(f"Bind {self.__class__}.on_trigger={on_trigger}") + setattr(self.__class__, "on_trigger", on_trigger) + + self.debug(f"Bind {self.__class__}.on_progress={on_progress}") + setattr(self.__class__, "on_progress", on_progress) + + fn = partial(self.update, name=self.name, key="canvas") + Clock.schedule_once(fn, 0) + + def update(self, *args, **kwargs): + """Update screen with version key. Should be called before `on_enter`""" + name = kwargs.get("name") + key = kwargs.get("key") + value = kwargs.get("value") + + if name in ( + "ConfigKruxInstaller", + "DownloadStableZipSigScreen", + "DownloadSelfcustodyPemScreen", + ): + self.debug(f"Updating {self.name} from {name}...") + else: + self.redirect_error(f"Invalid screen name: {name}") + return + + if key == "locale": + self.locale = value + + elif key == "canvas": + # prepare background + with self.canvas.before: + Color(0, 0, 0, 1) + Rectangle(size=(Window.width, Window.height)) + + elif key == "public-key-certificate": + if value is None: + self.downloader = PemDownloader( + destdir=DownloadSelfcustodyPemScreen.get_destdir_assets() + ) + + if self.downloader is not None: + url = getattr(self.downloader, "url") + destdir = getattr(self.downloader, "destdir") + downloading = self.translate("Downloading") + to = self.translate("to") + filepath = os.path.join(destdir, "selfcustoduuy.pem") + + self.ids[f"{self.id}_info"].text = "".join( + [ + f"[size={self.SIZE_MP}sp]", + downloading, + "\n", + f"[color=#00AABB][ref={url}]{url}[/ref][/color]", + "\n", + to, + "\n", + filepath, + "[/size]", + ] + ) + + else: + self.redirect_error(f"Invalid value for key '{key}': '{value}'") + + elif key == "progress": + # calculate percentage of download + lens = [value["downloaded_len"], value["content_len"]] + + percent = lens[0] / lens[1] + + # for some unknow reason (yet) + # the screen show that downloaded + # 130B of 128B, so limit it to 128 + if percent > 1.0: + percent = 1.0 + + if lens[0] > lens[1]: + lens[0] = lens[1] + + of = self.translate("of") + self.ids[f"{self.id}_progress"].text = "".join( + [ + f"[size={self.SIZE_G}sp][b]{percent * 100:,.2f} %[/b][/size]", + "\n", + f"[size={self.SIZE_MP}sp]", + str(lens[0]), + f" {of} ", + str(lens[1]), + " B", + "[/size]", + ] + ) + + if percent == 1.00: + if self.downloader is not None: + destdir = getattr(self.downloader, "destdir") + downloaded = self.translate("downloaded") + filepath = os.path.join(destdir, "selfcustody.pem") + self.ids[f"{self.id}_info"].text = "".join( + [ + f"[size={self.SIZE_MP}sp]", + filepath, + "\n", + downloaded, + "[/size]", + ] + ) + + # When finish, change the label, wait some seconds + # and then change screen + self.trigger() + + else: + self.redirect_error(f'Invalid key: "{key}"') diff --git a/src/app/screens/download_stable_zip_screen.py b/src/app/screens/download_stable_zip_screen.py new file mode 100644 index 00000000..72deb462 --- /dev/null +++ b/src/app/screens/download_stable_zip_screen.py @@ -0,0 +1,203 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +download_stable_zip_screen.py +""" +import os +import time +from functools import partial +from kivy.app import App +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.graphics.vertex_instructions import Rectangle +from kivy.graphics.context_instructions import Color +from src.app.screens.base_download_screen import BaseDownloadScreen +from src.utils.downloader.zip_downloader import ZipDownloader + + +class DownloadStableZipScreen(BaseDownloadScreen): + """DownloadStableZipScreen download a official krux zip release""" + + def __init__(self, **kwargs): + super().__init__( + wid="download_stable_zip_screen", name="DownloadStableZipScreen", **kwargs + ) + self.to_screen = "DownloadStableZipSha256Screen" + + # Define some staticmethods in + # dynamic way, so they can be + # called in `on_enter` method of + # BaseDownloadScreen and in tests + + # This is a function that will be called + # when the download thread is finished + def on_trigger(dt): + time.sleep(2.1) + screen = self.manager.get_screen(self.to_screen) + fn = partial( + screen.update, name=self.name, key="version", value=self.version + ) + Clock.schedule_once(fn, 0) + self.set_screen(name=self.to_screen, direction="left") + + # This is a function that will be called + # when a bunch of data are streamed from github + def on_progress(data: bytes): + if self.downloader is not None: + fn = partial( + self.update, + name=self.name, + key="progress", + value={ + "downloaded_len": self.downloader.downloaded_len, + "content_len": self.downloader.content_len, + }, + ) + Clock.schedule_once(fn, 0) + + else: + self.redirect_error(f"Invalid downloader: {self.downloader}") + + # Now define the functions as staticmethods of class + self.debug(f"Bind {self.__class__}.on_trigger={on_trigger}") + setattr(self.__class__, "on_trigger", on_trigger) + + self.debug(f"Bind {self.__class__}.on_progress={on_progress}") + setattr(self.__class__, "on_progress", on_progress) + + # Once finished, update canvas + fn = partial(self.update, name=self.name, key="canvas") + Clock.schedule_once(fn, 0) + + def update(self, *args, **kwargs): + """Update screen with version key. Should be called before `on_enter`""" + name = kwargs.get("name") + key = kwargs.get("key") + value = kwargs.get("value") + + if name in ( + "ConfigKruxInstaller", + "MainScreen", + "WarningAlreadyDownloadedScreen", + "DownloadStableZipScreen", + ): + self.debug(f"Updating {self.name} from {name}...") + else: + self.redirect_error(f"Invalid screen name: {name}") + return + + if key == "locale": + if value is not None: + self.locale = value + else: + self.redirect_error(f"Invalid value for key '{key}': '{value}'") + + elif key == "canvas": + # prepare background + with self.canvas.before: + Color(0, 0, 0, 1) + Rectangle(size=(Window.width, Window.height)) + + elif key == "version": + if value is not None: + self.version = value + self.downloader = ZipDownloader( + version=self.version, + destdir=DownloadStableZipScreen.get_destdir_assets(), + ) + + if self.downloader is not None: + url = getattr(self.downloader, "url") + destdir = getattr(self.downloader, "destdir") + downloading = self.translate("Downloading") + to = self.translate("to") + filepath = os.path.join(destdir, f"krux-{self.version}.zip") + + self.ids[f"{self.id}_info"].text = "".join( + [ + f"[size={self.SIZE_MP}sp]", + downloading, + "\n", + f"[color=#00AABB][ref={url}]{url}[/ref][/color]", + "\n", + to, + "\n", + filepath, + "[/size]", + ] + ) + + else: + self.redirect_error("Invalid downloader") + + else: + self.redirect_error(f"Invalid value for key '{key}': '{value}'") + + elif key == "progress": + if value is not None: + # calculate percentage of download + lens = [value["downloaded_len"], value["content_len"]] + percent = lens[0] / lens[1] + + # Format bytes (one liner) in MB + # https://stackoverflow.com/questions/ + # 5194057/better-way-to-convert-file-sizes-in-python#answer-52684562 + downs = [f"{lens[0]/(1<<20):,.2f}", f"{lens[1]/(1<<20):,.2f}"] + + of = self.translate("of") + self.ids[f"{self.id}_progress"].text = "".join( + [ + f"[size={self.SIZE_G}sp][b]{ percent * 100:,.2f} %[/b][/size]", + "\n", + f"[size={self.SIZE_MP}sp]", + downs[0], + f" {of} ", + downs[1], + " MB", + "[/size]", + ] + ) + + # When finish, change the label + # and then change screen + if percent == 1.00: + if self.downloader is not None: + destdir = getattr(self.downloader, "destdir") + downloaded = self.translate("downloaded") + filepath = os.path.join(destdir, f"krux-{self.version}.zip") + self.ids[f"{self.id}_info"].text = "".join( + [ + f"[size={self.SIZE_MP}sp]", + filepath, + "\n", + downloaded, + "[/size]", + ] + ) + # When finish, change the label, wait some seconds + # and then change screen + self.trigger() + + else: + self.redirect_error(f"Invalid downloader: {self.downloader}") + + else: + self.redirect_error(f'Invalid key: "{key}"') diff --git a/src/app/screens/download_stable_zip_sha256_screen.py b/src/app/screens/download_stable_zip_sha256_screen.py new file mode 100644 index 00000000..4baf9085 --- /dev/null +++ b/src/app/screens/download_stable_zip_sha256_screen.py @@ -0,0 +1,190 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +download_stable_zip_sha256_screen.py +""" +import os +import time +from functools import partial +from kivy.app import App +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.graphics.vertex_instructions import Rectangle +from kivy.graphics.context_instructions import Color +from src.app.screens.base_screen import BaseScreen +from src.app.screens.base_download_screen import BaseDownloadScreen +from src.utils.downloader.sha256_downloader import Sha256Downloader + + +class DownloadStableZipSha256Screen(BaseDownloadScreen): + """DownloadStableZipSha256Screen download the sha256sum file for official krux zip release""" + + def __init__(self, **kwargs): + super().__init__( + wid="download_stable_zip_sha256_screen", + name="DownloadStableZipSha256Screen", + **kwargs, + ) + self.to_screen = "DownloadStableZipSigScreen" + + # Define some staticmethods in dynamic way + # (so they can be called in tests) + def on_trigger(dt): + time.sleep(2.1) + screen = self.manager.get_screen(self.to_screen) + fn = partial( + screen.update, name=self.name, key="version", value=self.version + ) + Clock.schedule_once(fn, 0) + self.set_screen(name=self.to_screen, direction="left") + + def on_progress(data: bytes): + # calculate downloaded percentage + fn = partial( + self.update, + name=self.name, + key="progress", + value={ + "downloaded_len": self.downloader.downloaded_len, + "content_len": self.downloader.content_len, + }, + ) + Clock.schedule_once(fn, 0) + + self.debug(f"Bind {self.__class__}.on_trigger={on_trigger}") + setattr(self.__class__, "on_trigger", on_trigger) + + self.debug(f"Bind {self.__class__}.on_progress={on_progress}") + setattr(self.__class__, "on_progress", on_progress) + + fn = partial(self.update, name=self.name, key="canvas") + Clock.schedule_once(fn, 0) + + def update(self, *args, **kwargs): + """Update screen with version key. Should be called before `on_enter`""" + name = kwargs.get("name") + key = kwargs.get("key") + value = kwargs.get("value") + + if name in ( + "ConfigKruxInstaller", + "DownloadStableZipScreen", + "DownloadStableZipSha256Screen", + ): + self.debug(f"Updating {self.name} from {name}...") + else: + self.redirect_error(f"Invalid screen name: {name}") + return + + if key == "locale": + if value is not None: + self.locale = value + else: + self.redirect_error("Invalid value for 'key': '{value}'") + + elif key == "canvas": + # prepare background + with self.canvas.before: + Color(0, 0, 0, 1) + Rectangle(size=(Window.width, Window.height)) + + elif key == "version": + if value is not None: + self.version = value + self.downloader = Sha256Downloader( + version=value, + destdir=DownloadStableZipSha256Screen.get_destdir_assets(), + ) + + if self.downloader is not None: + url = getattr(self.downloader, "url") + destdir = getattr(self.downloader, "destdir") + downloading = self.translate("Downloading") + to = self.translate("to") + filepath = os.path.join( + destdir, f"krux-{self.version}.zip.sha256.txt" + ) + + self.ids[f"{self.id}_info"].text = "".join( + [ + f"[size={self.SIZE_MP}sp]", + downloading, + "\n", + f"[color=#00AABB][ref={url}]{url}[/ref][/color]", + "\n", + to, + "\n", + filepath, + "[/size]", + ] + ) + else: + self.redirect_error(f"Invalid value for key '{key}': '{value}'") + + elif key == "progress": + if value is not None: + # calculate percentage of download + lens = [value["downloaded_len"], value["content_len"]] + percent = lens[0] / lens[1] + + of = self.translate("of") + self.ids[f"{self.id}_progress"].text = "".join( + [ + f"[size={self.SIZE_G}sp][b]{percent * 100:,.2f} %[/b][/size]", + "\n", + f"[size={self.SIZE_MP}sp]", + str(lens[0]), + f" {of} ", + str(lens[1]), + " B", + "[/size]", + ] + ) + + # When finish, change the label + # and then change screen + if percent == 1.00: + if self.downloader is not None: + destdir = getattr(self.downloader, "destdir") + downloaded = self.translate("downloaded") + filepath = os.path.join( + destdir, f"krux-{self.version}.zip.sha256.txt" + ) + self.ids[f"{self.id}_info"].text = "".join( + [ + f"[size={self.SIZE_MP}sp]", + filepath, + "\n", + downloaded, + "[/size]", + ] + ) + # When finish, change the label, wait some seconds + # and then change screen + self.trigger() + + else: + self.redirect_error(f"Invalid downloader: {self.downloader}") + else: + self.redirect_error(f"Invalid value for key '{key}': '{value}'") + + else: + self.redirect_error(f'Invalid key: "{key}"') diff --git a/src/app/screens/download_stable_zip_sig_screen.py b/src/app/screens/download_stable_zip_sig_screen.py new file mode 100644 index 00000000..a576d781 --- /dev/null +++ b/src/app/screens/download_stable_zip_sig_screen.py @@ -0,0 +1,184 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +download_stable_zip_sig_screen.py +""" +import os +import time +from functools import partial +from kivy.app import App +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.graphics.vertex_instructions import Rectangle +from kivy.graphics.context_instructions import Color +from src.app.screens.base_screen import BaseScreen +from src.app.screens.base_download_screen import BaseDownloadScreen +from src.utils.downloader.sig_downloader import SigDownloader + + +class DownloadStableZipSigScreen(BaseDownloadScreen): + """DownloadStableZipSigScreen download the sig file for official krux zip release""" + + def __init__(self, **kwargs): + super().__init__( + wid="download_stable_zip_sig_screen", + name="DownloadStableZipSigScreen", + **kwargs, + ) + self.to_screen = "DownloadSelfcustodyPemScreen" + + # Define some staticmethods in dynamic way + # (so they can be called in tests) + def on_trigger(dt): + time.sleep(2.1) + screen = self.manager.get_screen(self.to_screen) + fn = partial(screen.update, name=self.name, key="public-key-certificate") + Clock.schedule_once(fn, 0) + self.set_screen(name=self.to_screen, direction="left") + + def on_progress(data: bytes): + # calculate downloaded percentage + fn = partial( + self.update, + name=self.name, + key="progress", + value={ + "downloaded_len": self.downloader.downloaded_len, + "content_len": self.downloader.content_len, + }, + ) + Clock.schedule_once(fn, 0) + + self.debug(f"Bind {self.__class__}.on_trigger={on_trigger}") + setattr(self.__class__, "on_trigger", on_trigger) + + self.debug(f"Bind {self.__class__}.on_progress={on_progress}") + setattr(self.__class__, "on_progress", on_progress) + + fn = partial(self.update, name=self.name, key="canvas") + Clock.schedule_once(fn, 0) + + def update(self, *args, **kwargs): + """Update screen with version key. Should be called before `on_enter`""" + name = kwargs.get("name") + key = kwargs.get("key") + value = kwargs.get("value") + + if name in ( + "ConfigKruxInstaller", + "DownloadStableZipSha256Screen", + "DownloadStableZipSigScreen", + ): + self.debug(f"Updating {self.name} from {name}...") + else: + self.redirect_error(f"Invalid screen name: {name}") + return + + if key == "locale": + if value is not None: + self.locale = value + else: + self.redirect_error(f"Invalid value for key '{key}': '{value}'") + + elif key == "canvas": + # prepare background + with self.canvas.before: + Color(0, 0, 0, 1) + Rectangle(size=(Window.width, Window.height)) + + elif key == "version": + if value is not None: + self.version = value + self.downloader = SigDownloader( + version=self.version, + destdir=DownloadStableZipSigScreen.get_destdir_assets(), + ) + + if self.downloader is not None: + url = getattr(self.downloader, "url") + destdir = getattr(self.downloader, "destdir") + downloading = self.translate("Downloading") + to = self.translate("to") + filepath = os.path.join(destdir, f"krux-{self.version}.zip.sig") + + self.ids[f"{self.id}_info"].text = "".join( + [ + f"[size={self.SIZE_MP}sp]", + downloading, + "\n", + f"[color=#00AABB][ref={url}]{url}[/ref][/color]", + "\n", + to, + "\n", + filepath, + "[/size]", + ] + ) + + else: + self.redirect_error(f"Invalid value for key '{key}': '{value}'") + + elif key == "progress": + if value is not None: + # calculate percentage of download + lens = [value["downloaded_len"], value["content_len"]] + percent = lens[0] / lens[1] + + of = self.translate("of") + self.ids[f"{self.id}_progress"].text = "".join( + [ + f"[size={self.SIZE_G}sp][b]{percent * 100:,.2f} %[/b][/size]", + "\n", + f"[size={self.SIZE_MP}sp]", + str(lens[0]), + f" {of} ", + str(lens[1]), + " B", + "[/size]", + ] + ) + + # When finish, change the label + # and then change screen + if percent == 1.00: + if self.downloader is not None: + destdir = getattr(self.downloader, "destdir") + downloaded = self.translate("downloaded") + filepath = os.path.join(destdir, f"krux-{self.version}.zip.sig") + self.ids[f"{self.id}_info"].text = "".join( + [ + f"[size={self.SIZE_MP}sp]", + filepath, + "\n", + downloaded, + "[/size]", + ] + ) + + # When finish, change the label, wait some seconds + # and then change screen + self.trigger() + + else: + self.redirect_error(f"Invalid value for key '{key}': '{value}'") + + else: + self.redirect_error(f'Invalid key: "{key}"') diff --git a/src/app/screens/error_screen.py b/src/app/screens/error_screen.py new file mode 100644 index 00000000..91babf31 --- /dev/null +++ b/src/app/screens/error_screen.py @@ -0,0 +1,145 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +error_screen.py +""" +import traceback +from functools import partial +from typing import Text +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.graphics.vertex_instructions import Rectangle +from kivy.graphics.context_instructions import Color +from kivy.app import App +from kivy.cache import Cache +from src.app.screens.base_screen import BaseScreen +from src.utils.selector import Selector +from src.i18n import T + + +class ErrorScreen(BaseScreen): + """ + CheckInternetConnectionScreen will check internet connection and get the + latest release if ok + """ + + def __init__(self, **kwargs): + super().__init__( + wid="error_screen", + name="ErrorScreen", + **kwargs, + ) + self.src_code = "https://github.com/selfcustody/krux-installer" + + # Build grid where buttons will be placed + self.make_grid(wid=f"{self.id}_grid", rows=1) + + # START of on_press buttons + def _press(instance): + self.debug(f"Calling Button::{instance.id}::on_press") + self.set_background(wid=instance.id, rgba=(0.25, 0.25, 0.25, 1)) + + def _release(instance): + self.debug(f"Calling Button::{instance.id}::on_release") + self.set_background(wid=f"{instance.id}", rgba=(0, 0, 0, 1)) + self.set_screen(name="GreetingsScreen", direction="right") + + self.make_button( + row=0, + id=f"{self.id}_button", + root_widget=f"{self.id}_grid", + text="", + markup=True, + on_press=_press, + on_release=_release, + ) + fn = partial(self.update, name=self.name, key="canvas") + Clock.schedule_once(fn, 0) + + def update(self, *args, **kwargs): + """ + In linux, will check for user permission on group + dialout (debian-li ke) and uucp (archlinux-like) and + add user to that group to allow sudoless flash + """ + name = kwargs.get("name") + key = kwargs.get("key") + value = kwargs.get("value") + + if name in ( + "CheckPermissionsScreen", + "ConfigKruxInstaller", + "CheckInternetConnectionScreen", + "SelectDeviceScreen", + "SelectVersionScreen", + "SelectOldVersionScreen", + "DownloadBetaScreen", + "DownloadSelfcustodyPemScreen", + "FlashScreen", + "WipeScreen", + "AboutScreen", + "ErrorScreen", + ): + self.debug(f"Updating {self.name} from {name}...") + else: + raise ValueError(f"Invalid screen: {name}") + + if key == "locale": + self.locale = value + + elif key == "canvas": + # prepare background + with self.canvas.before: + Color(0, 0, 0, 1) + Rectangle(size=(Window.width, Window.height)) + + elif key == "error": + self.error(str(value.__context__)) + stack = [msg for msg in str(value.__context__).split(":") if len(msg) < 120] + + self.ids[f"{self.id}_button"].text = "\n".join( + [ + f"[size={self.SIZE_M}sp][color=#ff0000]{stack[0]}[/color][/size]", + f"[size={self.SIZE_MP}sp][color=#efcc00]{"\n".join(stack[1:])}[/color][/size]", + "", + "", + f"[size={self.SIZE_P}sp]Report issue at", + "".join( + [ + "[color=#00aabb]", + f"[ref={self.src_code}/issues/]", + f"{self.src_code}/issues", + "[/ref]", + "[/color]", + ] + ), + "[/size]", + ] + ) + else: + exc_info = ValueError(f"Invalid key: '{key}'") + fn = partial(self.update, name=self.name, key="error", value=exc_info) + Clock.schedule_once(fn, 0) + + def on_enter(self): + """Simple update your canvas""" + fn = partial(self.update, name=self.name, key="canvas") + Clock.schedule_once(fn, 0) diff --git a/src/app/screens/flash_screen.py b/src/app/screens/flash_screen.py new file mode 100644 index 00000000..7af64da6 --- /dev/null +++ b/src/app/screens/flash_screen.py @@ -0,0 +1,308 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +main_screen.py +""" +import sys +import threading +import traceback +from functools import partial +from kivy.app import App +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.graphics.vertex_instructions import Rectangle +from kivy.graphics.context_instructions import Color +from src.app.screens.base_flash_screen import BaseFlashScreen +from src.utils.flasher import Flasher + + +class FlashScreen(BaseFlashScreen): + """Flash screen is where flash occurs""" + + def __init__(self, **kwargs): + super().__init__(wid="flash_screen", name="FlashScreen", **kwargs) + fn = partial(self.update, name=self.name, key="canvas") + Clock.schedule_once(fn, 0) + + def on_pre_enter(self): + self.ids[f"{self.id}_grid"].clear_widgets() + + def on_print_callback(*args, **kwargs): + text = " ".join(str(x) for x in args) + self.info(text) + text = text.replace( + "\x1b[32m\x1b[1m[INFO]\x1b[0m", "[color=#00ff00]INFO[/color]" + ) + text = text.replace( + "\x1b[33mISP loaded", "[color=#efcc00]ISP loaded[/color]" + ) + text = text.replace( + "\x1b[33mInitialize K210 SPI Flash", + "[color=#efcc00]Initialize K210 SPI Flash[/color]", + ) + text = text.replace("Flash ID: \x1b[33m", "Flash ID: [color=#efcc00]") + text = text.replace( + "\x1b[0m, unique ID: \x1b[33m", "[/color], unique ID: [color=#efcc00]" + ) + text = text.replace("\x1b[0m, size: \x1b[33m", "[/color], size: ") + text = text.replace("\x1b[0m MB", "[/color] MB") + text = text.replace("\x1b[0m", "") + text = text.replace("\x1b[33m", "") + text = text.replace("\rProgramming", "Programming") + + if "INFO" in text: + self.output.append(text) + if "Rebooting" in text: + self.trigger() + + elif "Programming BIN" in text: + self.output[-1] = text + + elif "*" in text: + self.output.append("*") + self.output.append("") + + if len(self.output) > 18: + del self.output[:1] + + self.ids[f"{self.id}_info"].text = "\n".join(self.output) + + def on_process_callback( + file_type: str, iteration: int, total: int, suffix: str + ): + percent = (iteration / total) * 100 + + if sys.platform in ("linux", "win32"): + sizes = [self.SIZE_M, self.SIZE_MP, self.SIZE_P] + else: + sizes = [self.SIZE_MM, self.SIZE_M, self.SIZE_MP] + + please = self.translate("PLEASE DO NOT UNPLUG YOUR DEVICE") + flashing = self.translate("Flashing") + at = self.translate("at") + + self.ids[f"{self.id}_progress"].text = "".join( + [ + f"[size={sizes[1]}sp][b]{please}[/b][/size]", + "\n", + f"[size={sizes[0]}sp]{percent:.2f} %[/size]", + "\n", + f"[size={sizes[2]}sp]", + f"{flashing} ", + "[color=#efcc00]", + "[b]", + file_type, + "[/b]", + "[/color]", + f" {at} ", + "[color=#efcc00]", + "[b]", + suffix, + "[/b]", + "[/color]", + "[/size]", + ] + ) + + def on_ref_press(*args): + if args[1] == "Back": + self.set_screen(name="MainScreen", direction="right") + + elif args[1] == "Quit": + App.get_running_app().stop() + + else: + self.redirect_error(f"Invalid ref: {args[1]}") + + def on_trigger_callback(dt): + del self.output[4:] + self.ids[f"{self.id}_loader"].source = self.done_img + self.ids[f"{self.id}_loader"].reload() + done = self.translate("DONE") + back = self.translate("Back") + quit = self.translate("Quit") + + if sys.platform in ("linux", "win32"): + size = self.SIZE_M + else: + size = self.SIZE_M + + self.ids[f"{self.id}_progress"].text = "".join( + [ + f"[size={size}sp][b]{done}![/b][/size]", + "\n", + "\n", + f"[size={size}sp]", + "[color=#00FF00]", + f"[ref=Back][u]{back}[/u][/ref]", + "[/color]", + " ", + "[color=#EFCC00]", + f"[ref=Quit][u]{quit}[/u][/ref]", + "[/color]", + ] + ) + self.ids[f"{self.id}_progress"].bind(on_ref_press=on_ref_press) + + setattr(FlashScreen, "on_print_callback", on_print_callback) + setattr(FlashScreen, "on_process_callback", on_process_callback) + setattr(FlashScreen, "on_trigger_callback", on_trigger_callback) + + self.make_subgrid( + wid=f"{self.id}_subgrid", rows=2, root_widget=f"{self.id}_grid" + ) + + self.make_image( + wid=f"{self.id}_loader", + source=self.warn_img, + root_widget=f"{self.id}_subgrid", + ) + + self.make_label( + wid=f"{self.id}_progress", + text="", + root_widget=f"{self.id}_subgrid", + markup=True, + halign="center", + ) + + self.make_label( + wid=f"{self.id}_info", + text="", + root_widget=f"{self.id}_grid", + markup=True, + halign="justify", + ) + + def on_enter(self): + """ + Event fired when the screen is displayed and the entering animation is complete. + """ + if hasattr(self, "flasher"): + self.output = [] + self.trigger = getattr(self.__class__, "on_trigger_callback") + self.flasher.ktool.__class__.print_callback = getattr( + self.__class__, "on_print_callback" + ) + on_process_callback = partial( + self.flasher.flash, + callback=getattr(self.__class__, "on_process_callback"), + ) + self.thread = threading.Thread(name=self.name, target=on_process_callback) + + if sys.platform in ("linux", "win32"): + sizes = [self.SIZE_M, self.SIZE_P] + else: + sizes = [self.SIZE_MM, self.SIZE_MP] + + # if anything wrong happen, show it + def hook(err): + msg = "".join( + traceback.format_exception( + err.exc_type, err.exc_value, err.exc_traceback + ) + ) + self.error(msg) + + back = self.translate("Back") + quit = self.translate("Quit") + self.ids[f"{self.id}_progress"].text = "".join( + [ + f"[size={sizes[0]}]", + "[color=#FF0000]Flash failed[/color]", + "[/size]", + "\n", + "\n", + f"[size={sizes[0]}]", + "[color=#00FF00]", + f"[ref=Back][u]{back}[/u][/ref][/color]", + " ", + "[color=#EFCC00]", + f"[ref=Quit][u]{quit}[/u][/ref]", + "[/color]", + "[/size]", + ] + ) + + self.ids[f"{self.id}_info"].text = "".join( + [f"[size={sizes[1]}]", msg, "[/size]"] + ) + + # hook what happened + threading.excepthook = hook + + # start thread + self.thread.start() + else: + self.redirect_error("Flasher isnt configured") + + def update(self, *args, **kwargs): + """Update screen with firmware key. Should be called before `on_enter`""" + name = kwargs.get("name") + key = kwargs.get("key") + value = kwargs.get("value") + + if name in ( + "ConfigKruxInstaller", + "UnzipStableScreen", + "DownloadBetaScreen", + "FlashScreen", + ): + self.debug(f"Updating {self.name} from {name}...") + else: + raise ValueError(f"Invalid screen name: {name}") + + key = kwargs.get("key") + value = kwargs.get("value") + + if key == "locale": + if value is not None: + self.locale = value + else: + self.redirect_error(f"Invalid value for key '{key}': '{value}'") + + elif key == "canvas": + # prepare background + with self.canvas.before: + Color(0, 0, 0, 1) + Rectangle(size=(Window.width, Window.height)) + + elif key == "baudrate": + if value is not None: + self.baudrate = value + else: + self.redirect_error(f"Invalid value for key '{key}': '{value}'") + + elif key == "firmware": + if value is not None: + self.firmware = value + else: + self.redirect_error(f"Invalid value for key '{key}': '{value}'") + + elif key == "flasher": + self.flasher = Flasher() + self.flasher.firmware = self.firmware + self.flasher.baudrate = self.baudrate + + elif key == "exception": + self.redirect_error("") + else: + self.redirect_error(f'Invalid key: "{key}"') diff --git a/src/app/screens/greetings_screen.py b/src/app/screens/greetings_screen.py new file mode 100644 index 00000000..ab401e8c --- /dev/null +++ b/src/app/screens/greetings_screen.py @@ -0,0 +1,108 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +greetings_screen.py +""" +import sys +from functools import partial +from kivy.clock import Clock +from kivy.graphics import Color +from kivy.graphics import Rectangle +from kivy.core.window import Window +from .base_screen import BaseScreen + + +class GreetingsScreen(BaseScreen): + """GreetingsScreen show Krux logo""" + + def __init__(self, **kwargs): + super().__init__( + wid="greetings_screen", + name="GreetingsScreen", + **kwargs, + ) + + # Build grid where buttons will be placed + self.make_grid(wid=f"{self.id}_grid", rows=1) + + # Build logo + self.make_image( + wid=f"{self.id}_logo", root_widget=f"{self.id}_grid", source=self.logo_img + ) + + def update(self, *args, **kwargs): + """Update to go to some screen (MainScreen or CheckPermissionsScreen)""" + name = kwargs.get("name") + key = kwargs.get("key") + value = kwargs.get("value") + + if name in ("GreetingsScreen", "KruxInstallerApp"): + self.debug(f"Updating {self.name} from {name}") + else: + raise ValueError(f"Invalid screen: {name}") + + if key == "change_screen": + if value is not None and value in ( + "MainScreen", + "CheckPermissionsScreen", + "CheckInternetConnectionScreen", + ): + self.set_screen(name=value, direction="left") + else: + raise ValueError(f"Invalid value for '{key}': {value}") + + elif key == "canvas": + + with self.canvas.before: + Color(0, 0, 0) + Rectangle(pos=(0, 0), size=Window.size) + + elif key == "check_permissions": + # check platform and if is linux, go to CheckPermissionsScreen, + # otherwise, go to MainScreen + + if sys.platform == "linux": + fn = partial( + self.update, + name=self.name, + key="change_screen", + value="CheckPermissionsScreen", + ) + + elif sys.platform == "darwin" or sys.platform == "win32": + fn = partial( + self.update, + name=self.name, + key="change_screen", + value="CheckInternetConnectionScreen", + ) + + else: + raise RuntimeError(f"Not implemented for {sys.platform}") + + Clock.schedule_once(fn, 2.1) + + else: + raise ValueError(f"Invalid key: '{key}'") + + def on_enter(self): + fn = partial(self.update, name=self.name, key="canvas") + Clock.schedule_once(fn, 0) diff --git a/src/app/screens/main_screen.py b/src/app/screens/main_screen.py new file mode 100644 index 00000000..26d3952f --- /dev/null +++ b/src/app/screens/main_screen.py @@ -0,0 +1,398 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +main_screen.py +""" +import os +import re +import typing +import sys +from functools import partial +from kivy.clock import Clock +from kivy.app import App +from .base_screen import BaseScreen +from src.utils.selector import VALID_DEVICES +from src.i18n import T + + +class MainScreen(BaseScreen): + """ + Main screen is the 'Home' page + + .. versionadded:: 0.0.2-alpha-1 + """ + + def __init__(self, **kwargs): + super().__init__(wid="main_screen", name="MainScreen", **kwargs) + + # Prepare some variables + self.device = "select a new one" + self.version = "select a new one" + self.will_flash = False + self.will_wipe = False + + # Build grid where buttons will be placed + self.make_grid(wid="main_screen_grid", rows=6) + + def on_change(instance, value): + self.debug(f"Updating text for {instance}") + instance.refresh() + + # Buttons will be defined in dynamic way + # so you will need to keep in mind that + # some binded methods need a special + # treatment in loops + buttons = [ + ( + "main_select_version", + "".join( + [ + f"{self.translate("Version")}: ", + "[color=#00AABB]", + self.translate(self.version), + "[/color]", + ] + ), + ), + ( + "main_select_device", + "".join( + [ + f"{self.translate("Device")}: ", + "[color=#00AABB]", + self.translate(self.device), + "[/color]", + ] + ), + ), + ("main_flash", f"[color=#333333]{self.translate("Flash")}[/color]"), + ("main_wipe", f"[color=#333333]{self.translate("Wipe")}[/color]"), + ("main_settings", self.translate("Settings")), + ("main_about", self.translate("About")), + ] + + # START of buttons + for row, _tuple in enumerate(buttons): + + # START of on_press buttons + def _press(instance): + self.debug(f"Calling Button::{instance.id}::on_press") + if instance.id == "main_flash": + if self.will_flash: + self.set_background(wid=instance.id, rgba=(0.25, 0.25, 0.25, 1)) + else: + self.warning(f"Button::{instance.id} disabled") + + if instance.id == "main_wipe": + if self.will_wipe: + self.set_background(wid=instance.id, rgba=(0.25, 0.25, 0.25, 1)) + else: + self.warning(f"Button::{instance.id} disabled") + + if instance.id == "main_select_version": + url = "https://api.github.com/repos/selfcustody/krux/releases" + self.ids[instance.id].text = "".join( + [ + f"[size={self.SIZE_M}sp]", + "[color=#efcc00]", + f"[b]{self.translate("Fetching data from")}[/b]", + "\n", + f"[size={self.SIZE_MP}sp]", + url, + "[/size]", + "[/color]", + ] + ) + + if instance.id in ( + "main_select_device", + "main_select_version", + "main_settings", + "main_about", + ): + self.set_background(wid=instance.id, rgba=(0.25, 0.25, 0.25, 1)) + + # END of on_press buttons + + # START of on_release_buttons + def _release(instance): + self.debug(f"Calling Button::{instance.id}::on_release") + if instance.id == "main_select_device": + select_device = self.manager.get_screen("SelectDeviceScreen") + fn = partial( + select_device.update, + name=self.name, + key="version", + value=self.version, + ) + Clock.schedule_once(fn, 0) + self.set_background(wid="main_select_device", rgba=(0, 0, 0, 1)) + self.set_screen(name="SelectDeviceScreen", direction="left") + + elif instance.id == "main_select_version": + select_version = self.manager.get_screen("SelectVersionScreen") + select_version.clear() + select_version.fetch_releases() + self.set_background(wid="main_select_version", rgba=(0, 0, 0, 1)) + self.set_screen(name="SelectVersionScreen", direction="left") + self.update(name=self.name, key="version", value=self.version) + + elif instance.id == "main_flash": + if self.will_flash: + # do a click effect + self.set_background(wid="main_flash", rgba=(0, 0, 0, 1)) + + # partials are functions that call `update` + # method in screen before go to them + partials = [] + + # Check if any release file exists + if re.findall(r"^v\d+\.\d+\.\d$", self.version): + resources = MainScreen.get_destdir_assets() + zipfile = os.path.join( + resources, f"krux-{self.version}.zip" + ) + if os.path.isfile(zipfile): + to_screen = "WarningAlreadyDownloadedScreen" + else: + to_screen = "DownloadStableZipScreen" + + screen = self.manager.get_screen(to_screen) + partials.append( + partial( + screen.update, + name=self.name, + key="version", + value=self.version, + ) + ) + + # check if release is beta + elif re.findall("^odudex/krux_binaries", self.version): + to_screen = "DownloadBetaScreen" + screen = self.manager.get_screen(to_screen) + partials.append( + partial( + screen.update, + name=self.name, + key="firmware", + value="kboot.kfpkg", + ) + ) + partials.append( + partial( + screen.update, + name=self.name, + key="device", + value=self.device, + ) + ) + partials.append( + partial(screen.update, name=self.name, key="downloader") + ) + + # Execute the partials + for fn in partials: + Clock.schedule_once(fn, 0) + + # Goto the selected screen + self.set_screen(name=to_screen, direction="left") + else: + self.debug(f"Button::{instance.id} disabled") + + elif instance.id == "main_wipe": + if self.will_wipe: + self.set_background(wid="main_wipe", rgba=(0, 0, 0, 1)) + self.set_screen(name="WarningWipeScreen", direction="left") + else: + self.debug(f"Button::{instance.id} disabled") + + elif instance.id == "main_settings": + self.set_background(wid="main_settings", rgba=(0, 0, 0, 1)) + MainScreen.open_settings() + + elif instance.id == "main_about": + self.set_background(wid="main_about", rgba=(0, 0, 0, 1)) + self.set_screen(name="AboutScreen", direction="left") + + # END of on_release buttons + + self.make_button( + row=row, + id=_tuple[0], + root_widget="main_screen_grid", + text=_tuple[1], + markup=True, + on_press=_press, + on_release=_release, + ) + + # END of buttons + + @property + def device(self) -> str: + """Getter for device property""" + return self._device + + @device.setter + def device(self, value: str): + self.debug(f"device = {value}") + self._device = value + + @property + def version(self) -> str: + """Getter for version property""" + return self._version + + @version.setter + def version(self, value: str): + """Setter for version property""" + self.debug(f"version = {value}") + self._version = value + + @property + def will_flash(self) -> bool: + """Getter for will_flash property""" + return self._will_flash + + @will_flash.setter + def will_flash(self, value: bool): + """Setter for will_flash property""" + self.debug(f"will_flash = {value}") + self._will_flash = value + + @property + def will_wipe(self) -> bool: + """Getter for will_wipe property""" + return self._will_wipe + + @will_wipe.setter + def will_wipe(self, value: bool): + """Setter for will_wipe property""" + self.debug(f"will_wipe = {value}") + self._will_wipe = value + + def update(self, *args, **kwargs): + """Update buttons from selected device/versions on related screens""" + name = kwargs.get("name") + key = kwargs.get("key") + value = kwargs.get("value") + + # Check if update to screen + if name in ( + "KruxInstallerApp", + "ConfigKruxInstaller", + "MainScreen", + "SelectDeviceScreen", + "SelectVersionScreen", + "SelectOldVersionScreen", + ): + self.debug(f"Updating {self.name} from {name}...") + else: + self.redirect_error(msg=f"Invalid screen name: {name}") + + # Check locale + if key == "locale": + if value is not None: + self.locale = value + + else: + self.redirect_error(f"Invalid value for key {key}: {value}") + + # Check if update to given key + elif key == "version": + + if value is not None: + self.version = MainScreen.sanitize_markup(value) + self.ids["main_select_version"].text = "".join( + [ + f"{self.translate("Version")}: ", + "[color=#00AABB]", + self.version, + "[/color]", + ] + ) + else: + self.redirect_error(f"Invalid value for key '{key}': '{value}'") + + elif key == "device": + + if value is not None: + value = MainScreen.sanitize_markup(value) + + # check if update to given values + if value in VALID_DEVICES: + self.device = value + self.will_flash = True + self.will_wipe = True + self.ids["main_flash"].text = self.translate("Flash") + self.ids["main_wipe"].text = self.translate("Wipe") + + else: + self.will_flash = False + self.will_wipe = False + self.ids["main_flash"].markup = True + self.ids["main_wipe"].markup = True + self.ids["main_flash"].text = "".join( + ["[color=#333333]", self.translate("Flash"), "[/color]"] + ) + self.ids["main_wipe"].text = "".join( + ["[color=#333333]", self.translate("Wipe"), "[/color]"] + ) + + if value == "select a new one": + value = self.translate("select a new one") + + self.ids["main_select_device"].text = "".join( + [ + f"{self.translate("Device")}: ", + "[color=#00AABB]", + value, + "[/color]", + ] + ) + else: + self.redirect_error(f"Invalid value for key '{key}': '{value}'") + + elif key == "flash": + if not self.will_flash: + self.ids["main_flash"].text = "".join( + ["[color=#333333]", self.translate("Flash"), "[/color]"] + ) + else: + self.ids["main_flash"].text = self.translate("Flash") + + elif key == "wipe": + if not self.will_wipe: + self.ids["main_wipe"].text = "".join( + ["[color=#333333]", self.translate("Wipe"), "[/color]"] + ) + else: + self.ids["main_wipe"].text = self.translate("Wipe") + + elif key == "settings": + self.ids["main_settings"].text = self.translate("Settings") + + elif key == "about": + self.ids["main_about"].text = self.translate("About") + + else: + self.redirect_error(msg=f'Invalid key: "{key}"') diff --git a/src/app/screens/select_device_screen.py b/src/app/screens/select_device_screen.py new file mode 100644 index 00000000..7e5d02f4 --- /dev/null +++ b/src/app/screens/select_device_screen.py @@ -0,0 +1,124 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +select_device_screen.py +""" +import re +from functools import partial +from kivy.clock import Clock +from kivy.cache import Cache +from kivy.weakproxy import WeakProxy +from kivy.core.window import Window +from kivy.uix.button import Button +from kivy.graphics import Color, Line +from src.utils.constants import VALID_DEVICES_VERSIONS +from src.app.screens.base_screen import BaseScreen + + +class SelectDeviceScreen(BaseScreen): + """SelectDeviceScreen is where versions can be selected""" + + def __init__(self, **kwargs): + super().__init__( + wid="select_device_screen", name="SelectDeviceScreen", **kwargs + ) + self.enabled_devices = [] + self.make_grid(wid="select_device_screen_grid", rows=7) + + for row, device in enumerate( + ["m5stickv", "amigo", "dock", "bit", "yahboom", "cube", "wonder_mv"] + ): + + def _on_press(instance): + if instance.id in self.enabled_devices: + self.debug(f"Calling Button::{instance.id}::on_press") + self.set_background(wid=instance.id, rgba=(0.25, 0.25, 0.25, 1)) + + def _on_release(instance): + if instance.id in self.enabled_devices: + self.debug(f"Calling Button::{instance.id}::on_release") + self.set_background(wid=instance.id, rgba=(0, 0, 0, 1)) + device = self.ids[instance.id].text + clean_device = SelectDeviceScreen.sanitize_markup(device) + self.debug(f"on_release::{instance.id} = {clean_device}") + main_screen = self.manager.get_screen("MainScreen") + fn = partial( + main_screen.update, + name=self.name, + key="device", + value=clean_device, + ) + Clock.schedule_once(fn, 0) + self.set_screen(name="MainScreen", direction="right") + + self.make_button( + row=row, + id=f"select_device_{device}", + root_widget="select_device_screen_grid", + text="", + markup=True, + on_press=_on_press, + on_release=_on_release, + ) + + def update(self, *args, **kwargs): + """Update buttons according the valid devices for each version""" + name = kwargs.get("name") + key = kwargs.get("key") + value = kwargs.get("value") + + # Check if update to screen + if name in ("ConfigKruxInstaller", "SelectDeviceScreen", "MainScreen"): + self.debug(f"Updating {self.name} from {name}...") + else: + self.redirect_error(f"Invalid screen name: {name}") + + if key == "version": + self.debug( + f"Updating buttons to fit {kwargs.get("key")} = {kwargs.get("version")}" + ) + + if value is not None: + self.enabled_devices = [] + + for device in ( + "m5stickv", + "amigo", + "dock", + "bit", + "yahboom", + "cube", + "wonder_mv", + ): + cleanr = re.compile("\\[.*?\\]") + clean_text = re.sub(cleanr, "", value) + if device not in VALID_DEVICES_VERSIONS[clean_text]: + self.ids[f"select_device_{device}"].text = "".join( + ["[color=#333333]", device, "[/color]"] + ) + else: + self.enabled_devices.append(f"select_device_{device}") + self.ids[f"select_device_{device}"].text = device + + else: + self.redirect_error(f"Invalid value for key '{key}': '{value}'") + else: + self.redirect_error(f"Invalid key: {key}") diff --git a/src/app/screens/select_old_version_screen.py b/src/app/screens/select_old_version_screen.py new file mode 100644 index 00000000..3d4b0445 --- /dev/null +++ b/src/app/screens/select_old_version_screen.py @@ -0,0 +1,135 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +select_old_version_screen.py +""" +# pylint: disable=no-name-in-module +import re +import typing +from functools import partial +from kivy.clock import Clock +from kivy.weakproxy import WeakProxy +from kivy.core.window import Window +from kivy.uix.button import Button +from kivy.graphics import Color, Line +from src.utils.selector import Selector +from .base_screen import BaseScreen + + +class SelectOldVersionScreen(BaseScreen): + """SelectOldVersionScreen is where old versions can be selected""" + + def __init__(self, **kwargs): + super().__init__( + wid="select_old_version_screen", name="SelectOldVersionScreen", **kwargs + ) + + def fetch_releases(self, old_versions: typing.List[str]): + """Build a set of buttons to select version""" + self.make_grid(wid="select_old_version_screen_grid", rows=len(old_versions) + 1) + self.clear_grid(wid="select_old_version_screen_grid") + + for row, text in enumerate(old_versions): + sanitized = (text.replace(".", "_").replace("/", "_"),) + wid = f"select_old_version_{sanitized}" + + def _press(instance): + self.debug(f"Calling {instance}::on_press") + self.set_background(wid=instance.id, rgba=(0.25, 0.25, 0.25, 1)) + + def _release(instance): + self.debug(f"Calling {instance.id}::on_release") + self.set_background(wid=instance.id, rgba=(0, 0, 0, 1)) + version = self.ids[instance.id].text + self.debug(f"on_release::{instance.id} = {version}") + main_screen = self.manager.get_screen("MainScreen") + fn_version = partial( + main_screen.update, name=self.name, key="version", value=version + ) + fn_device = partial( + main_screen.update, + name=self.name, + key="device", + value="select a new one", + ) + Clock.schedule_once(fn_version, 0) + Clock.schedule_once(fn_device, 0) + self.set_screen(name="MainScreen", direction="right") + + self.make_button( + row=row, + id=wid, + root_widget="select_old_version_screen_grid", + text=text, + markup=True, + on_press=_press, + on_release=_release, + ) + + # Back Button + def _press_back(instance): + self.debug(f"Calling {instance}::on_press") + self.set_background( + wid="select_old_version_back", rgba=(0.25, 0.25, 0.25, 1) + ) + + def _release_back(instance): + self.debug(f"Calling {instance}::on_release") + self.set_background(wid="select_old_version_back", rgba=(0, 0, 0, 1)) + self.set_screen(name="SelectVersionScreen", direction="right") + + back = self.translate("Back") + self.make_button( + row=len(old_versions) + 1, + id="select_old_version_back", + root_widget="select_old_version_screen_grid", + text=back, + markup=True, + on_press=_press_back, + on_release=_release_back, + ) + + def update(self, *args, **kwargs): + """Update buttons on related screen""" + name = kwargs.get("name") + key = kwargs.get("key") + value = kwargs.get("value") + + # Check if update to screen + if name in ("ConfigKruxInstaller", "SelectOldVersionScreen"): + self.debug(f"Updating {self.name} from {name}...") + else: + self.redirect_error(f"Invalid screen name: {name}") + + # Check locale + if key == "locale": + if value is not None: + self.locale = value + + if "select_old_version_back" in self.ids: + back = self.translate("Back") + self.ids["select_old_version_back"].text = back + + else: + self.redirect_error(f"Invalid value for key '{key}': '{value}'") + + else: + self.redirect_error(msg=f'Invalid key: "{key}"') diff --git a/src/app/screens/select_version_screen.py b/src/app/screens/select_version_screen.py new file mode 100644 index 00000000..efb4f460 --- /dev/null +++ b/src/app/screens/select_version_screen.py @@ -0,0 +1,183 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +select_version_screen.py +""" +# pylint: disable=no-name-in-module +import re +from functools import partial +from kivy.clock import Clock +from kivy.weakproxy import WeakProxy +from kivy.core.window import Window +from kivy.uix.button import Button +from kivy.graphics import Color, Line +from src.utils.selector import Selector +from src.app.screens.base_screen import BaseScreen + + +class SelectVersionScreen(BaseScreen): + """Flash screen is where flash occurs""" + + def __init__(self, **kwargs): + super().__init__( + wid="select_version_screen", name="SelectVersionScreen", **kwargs + ) + + # Build grid where buttons will be placed + self.make_grid(wid="select_version_screen_grid", rows=4) + + def clear(self): + """Clear the list of children widgets buttons""" + self.ids["select_version_screen_grid"].clear_widgets() + + def fetch_releases(self): + """Build a set of buttons to select version""" + try: + selector = Selector() + + old = self.translate("Old versions") + back = self.translate("Back") + + buttons = [ + ( + "select_version_latest", + selector.releases[0], + ), + ( + "select_version_beta", + selector.releases[-1], + ), + ( + "select_version_old", + old, + ), + ("select_version_back", back), + ] + + # Push other releases to SelectOldVersionScreen + select_old_version_screen = self.manager.get_screen( + "SelectOldVersionScreen" + ) + select_old_version_screen.fetch_releases( + old_versions=selector.releases[1:-1] + ) + # START of buttons + for row, _tuple in enumerate(buttons): + + # START of on_press buttons + def _press(instance): + self.debug(f"Calling Button::{instance.id}::on_press") + self.set_background(wid=instance.id, rgba=(0.25, 0.25, 0.25, 1)) + + # START of on_release_buttons + def _release(instance): + self.debug(f"Calling Button::{instance.id}::on_release") + self.set_background(wid=instance.id, rgba=(0, 0, 0, 1)) + + if instance.id == "select_version_latest": + version = self.ids[instance.id].text + self.debug(f"on_release::{instance.id} = {version}") + main_screen = self.manager.get_screen("MainScreen") + fn_version = partial( + main_screen.update, + name=self.name, + key="version", + value=version, + ) + fn_device = partial( + main_screen.update, + name=self.name, + key="device", + value="select a new one", + ) + Clock.schedule_once(fn_version, 0) + Clock.schedule_once(fn_device, 0) + self.set_screen(name="MainScreen", direction="right") + + if instance.id == "select_version_beta": + version = self.ids[instance.id].text + main_screen = self.manager.get_screen("MainScreen") + fn_version = partial( + main_screen.update, + name=self.name, + key="version", + value="odudex/krux_binaries", + ) + fn_device = partial( + main_screen.update, + name=self.name, + key="device", + value="select a new one", + ) + Clock.schedule_once(fn_version, 0) + Clock.schedule_once(fn_device, 0) + self.debug(f"on_release::{instance.id} = {version}") + self.set_screen(name="WarningBetaScreen", direction="left") + + if instance.id == "select_version_old": + self.set_screen(name="SelectOldVersionScreen", direction="left") + + if instance.id == "select_version_back": + self.set_screen(name="MainScreen", direction="right") + + # END of on_release buttons + self.make_button( + row=row, + id=_tuple[0], + root_widget="select_version_screen_grid", + text=_tuple[1], + markup=True, + on_press=_press, + on_release=_release, + ) + + except Exception as exc: + self.redirect_exception(exception=exc) + + def update(self, *args, **kwargs): + """Update buttons on related screen""" + name = kwargs.get("name") + key = kwargs.get("key") + value = kwargs.get("value") + + # Check if update to screen + if name in ("ConfigKruxInstaller", "SelectVersionScreen"): + self.debug(f"Updating {self.name} from {name}...") + else: + self.redirect_error(f"Invalid screen name: {name}") + + # Check locale + if key == "locale": + if value is not None: + self.locale = value + if "select_version_old" in self.ids: + old = self.translate("Old versions") + self.ids["select_version_old"].text = old + + if "select_version_back" in self.ids: + back = self.translate("Back") + self.ids["select_version_back"].text = back + + else: + self.redirect_error(f"Invalid value for key '{key}': '{value}'") + + else: + self.redirect_error(f'Invalid key: "{key}"') diff --git a/src/app/screens/settings_screen.py b/src/app/screens/settings_screen.py new file mode 100644 index 00000000..4e69475a --- /dev/null +++ b/src/app/screens/settings_screen.py @@ -0,0 +1,32 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +main_screen.py +""" + +from .base_screen import BaseScreen + + +class SettingsScreen(BaseScreen): + """Flash screen is where flash occurs""" + + def __init__(self, **kwargs): + super().__init__(wid="settings_screen", name="SettingsScreen", **kwargs) diff --git a/src/app/screens/unzip_stable_screen.py b/src/app/screens/unzip_stable_screen.py new file mode 100644 index 00000000..9e598ca0 --- /dev/null +++ b/src/app/screens/unzip_stable_screen.py @@ -0,0 +1,302 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +verify_stable_zip_screen.py +""" +import os +import sys +import time +from functools import partial +from kivy.clock import Clock +from kivy.graphics.vertex_instructions import Rectangle +from kivy.graphics.context_instructions import Color +from kivy.app import App +from kivy.core.window import Window +from kivy.weakproxy import WeakProxy +from kivy.uix.stacklayout import StackLayout +from kivy.uix.button import Button +from src.utils.constants import get_name, get_version +from src.app.screens.base_screen import BaseScreen +from src.utils.unzip.kboot_unzip import KbootUnzip +from src.utils.unzip.firmware_unzip import FirmwareUnzip + + +class UnzipStableScreen(BaseScreen): + """VerifyStableZipScreen check for sha256sum and siganture""" + + def __init__(self, **kwargs): + super().__init__(wid="unzip_stable_screen", name="UnzipStableScreen", **kwargs) + self.make_grid(wid=f"{self.id}_grid", rows=2) + self.assets_dir = UnzipStableScreen.get_destdir_assets() + self.device = None + self.version = None + + def update(self, *args, **kwargs): + """Update widget from other screens""" + + name = kwargs.get("name") + key = kwargs.get("key") + value = kwargs.get("value") + + # Check if update to screen + if name in ( + "ConfigKruxInstaller", + "VerifyStableZipScreen", + "UnzipStableScreen", + ): + self.debug(f"Updating {self.name} from {name}...") + else: + raise ValueError(f"Invalid screen name: {name}") + + # Check locale + if key == "locale": + if value is not None: + self.locale = value + else: + self.redirect_error(f"Invalid value for key '{key}': '{value}'") + + elif key == "version": + if value is not None: + self.version = value + else: + self.redirect_error(f"Invalid value for key '{key}': '{value}'") + + elif key == "device": + if value is not None: + self.device = value + else: + self.redirect_error(f"Invalid value for key '{key}': '{value}'") + + elif key == "clear": + self.debug(f"Clearing '{self.id}_grid'") + self.ids[f"{self.id}_grid"].clear_widgets() + + elif key == "flash-button": + self.build_extract_to_flash_button() + + elif key == "airgap-button": + self.build_extract_to_airgap_button() + + else: + self.redirect_error(f'Invalid key: "{key}"') + + def build_extract_to_flash_button(self): + self.debug("Building flash button") + zip_file = os.path.join(self.assets_dir, f"krux-{self.version}.zip") + base_path = os.path.join(f"krux-{self.version}", f"maixpy_{self.device}") + rel_path = os.path.join(self.assets_dir, base_path) + flash_msg = self.translate("Flash with") + extract_msg = self.translate("Unziping") + extracted_msg = self.translate("Unziped") + size = [self.SIZE_MM, self.SIZE_MP] + + def _press(instance): + self.debug(f"Calling Button::{instance.id}::on_press") + file_path = os.path.join(rel_path, "kboot.kfpkg") + self.ids[instance.id].text = "".join( + [ + f"[size={size[0]}sp]", + extract_msg, + "[/size]", + "\n", + f"[size={size[1]}sp]", + "[color=#efcc00]", + file_path, + "[/color]", + "[/size]", + ] + ) + self.set_background(wid=instance.id, rgba=(0.25, 0.25, 0.25, 1)) + + def _release(instance): + self.debug(f"Calling Button::{instance.id}::on_release") + if self.device is not None: + file_path = os.path.join(base_path, "kboot.kfpkg") + full_path = os.path.join(self.assets_dir, file_path) + baudrate = UnzipStableScreen.get_baudrate() + + unziper = KbootUnzip( + filename=zip_file, device=self.device, output=self.assets_dir + ) + + # load variables to FlashScreen before get in + screen = self.manager.get_screen("FlashScreen") + fns = [ + partial( + screen.update, name=self.name, key="firmware", value=full_path + ), + partial( + screen.update, name=self.name, key="baudrate", value=baudrate + ), + partial(screen.update, name=self.name, key="flasher"), + ] + + for fn in fns: + Clock.schedule_once(fn, 0) + + # start the unzip process + self.set_background(wid=instance.id, rgba=(0, 0, 0, 1)) + unziper.load() + + # once unziped, give some messages + p = os.path.join(rel_path, "kboot.kfpkg") + self.ids[instance.id].text = "".join( + [ + f"[size={size[0]}sp]", + extracted_msg, + "[/size]", + "\n", + f"[size={size[1]}sp]", + "[color=#efcc00]", + p, + "[/color]", + "[/size]", + ] + ) + + time.sleep(2.1) + self.set_screen(name="FlashScreen", direction="left") + + setattr(UnzipStableScreen, f"on_press_{self.id}_flash_button", _press) + setattr(UnzipStableScreen, f"on_release_{self.id}_flash_button", _release) + + p = os.path.join(rel_path, "kboot.kfpkg") + self.make_button( + id=f"{self.id}_flash_button", + root_widget=f"{self.id}_grid", + text="".join( + [ + f"[size={size[0]}sp]", + flash_msg, + "[/size]", + "\n", + f"[size={size[1]}sp]", + "[color=#efcc00]", + p, + "[/color]", + "[/size]", + ] + ), + markup=True, + row=0, + on_press=getattr(UnzipStableScreen, f"on_press_{self.id}_flash_button"), + on_release=getattr(UnzipStableScreen, f"on_release_{self.id}_flash_button"), + ) + + def build_extract_to_airgap_button(self): + self.debug("Building airgap button") + zip_file = os.path.join(self.assets_dir, f"krux-{self.version}.zip") + base_path = os.path.join(f"krux-{self.version}", f"maixpy_{self.device}") + rel_path = os.path.join(self.assets_dir, base_path) + airgap_msg = self.translate("Air-gapped update with") + extract_msg = self.translate("Unziping") + extracted_msg = self.translate("Unziped") + + size = [self.SIZE_MM, self.SIZE_MP] + + activated = False + + def _press(instance): + if activated: + self.debug(f"Calling Button::{instance.id}::on_press") + file_path = os.path.join(rel_path, "firmware.bin") + self.ids[instance.id].text = "".join( + [ + f"[size={size[0]}sp]", + extract_msg, + "[/size]", + "\n", + f"[size={size[1]}sp]", + "[color=#efcc00]", + file_path, + "[/color]", + "[/size]", + ] + ) + self.set_background(wid=instance.id, rgba=(0.25, 0.25, 0.25, 1)) + + def _release(instance): + if activated: + self.debug(f"Calling Button::{instance.id}::on_release") + if self.device is not None: + file_path = f"{base_path}/firmware.bin" + unziper = FirmwareUnzip( + filename=zip_file, device=self.device, output=self.assets_dir + ) + + screen = self.manager.get_screen("AirgapScreen") + fns = [ + partial(screen.update, key="firmware", value=file_path), + partial(screen.update, key="device", value=self.device), + ] + for fn in fns: + Clock.schedule_once(fn, 0) + + self.set_background(wid=instance.id, rgba=(0, 0, 0, 1)) + unziper.load() + + p = os.path.join(rel_path, "firmware.bin") + self.ids[instance.id].text = "".join( + [ + f"[size={size[0]}sp]", + extracted_msg, + "[/size]", + "\n", + f"[size={size[1]}sp]", + "[color=#efcc00]", + p, + "[/color]", + "[/size]", + ] + ) + + time.sleep(2.1) + self.set_screen(name="AirgapScreen", direction="left") + + setattr(UnzipStableScreen, f"on_press_{self.id}_airgap_button", _press) + setattr(UnzipStableScreen, f"on_release_{self.id}_airgap_button", _release) + + p = os.path.join(rel_path, "firmware.bin") + self.make_button( + id=f"{self.id}_airgap_button", + root_widget=f"{self.id}_grid", + text="".join( + [ + f"[size={size[0]}sp]", + "[color=#333333]", + airgap_msg, + "[/color]", + "[/size]", + "\n", + f"[size={size[1]}sp]", + "[color=#333333]", + p, + "[/color]", + "[/size]", + ] + ), + markup=True, + row=0, + on_press=getattr(UnzipStableScreen, f"on_press_{self.id}_airgap_button"), + on_release=getattr( + UnzipStableScreen, f"on_release_{self.id}_airgap_button" + ), + ) diff --git a/src/app/screens/verify_stable_zip_screen.py b/src/app/screens/verify_stable_zip_screen.py new file mode 100644 index 00000000..8bb83ef4 --- /dev/null +++ b/src/app/screens/verify_stable_zip_screen.py @@ -0,0 +1,316 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +verify_stable_zip_screen.py +""" +import os +import sys +import time +from functools import partial +from kivy.clock import Clock +from kivy.graphics.vertex_instructions import Rectangle +from kivy.graphics.context_instructions import Color +from kivy.app import App +from kivy.core.window import Window +from kivy.weakproxy import WeakProxy +from kivy.uix.label import Label +from kivy.uix.stacklayout import StackLayout +from kivy.uix.button import Button +from src.utils.constants import get_name, get_version +from src.app.screens.base_screen import BaseScreen +from src.utils.verifyer.sha256_check_verifyer import Sha256CheckVerifyer +from src.utils.verifyer.sha256_verifyer import Sha256Verifyer +from src.utils.verifyer.sig_check_verifyer import SigCheckVerifyer +from src.utils.verifyer.sig_verifyer import SigVerifyer +from src.utils.verifyer.pem_check_verifyer import PemCheckVerifyer + + +class VerifyStableZipScreen(BaseScreen): + """VerifyStableZipScreen check for sha256sum and siganture""" + + def __init__(self, **kwargs): + super().__init__( + wid="verify_stable_zip_screen", name="VerifyStableZipScreen", **kwargs + ) + self.success = False + self.make_grid(wid=f"{self.id}_grid", rows=1) + + # instead make a button + # create a ref text that instead redirect + # to a web page, redirect to a screen + def _on_ref_press(*args): + self.debug(f"Calling ref::{args[0]}::on_ref_press") + self.debug(f"Opening {args[1]}") + + if args[1] == "UnzipStableScreen": + main_screen = self.manager.get_screen("MainScreen") + u = self.manager.get_screen("UnzipStableScreen") + + fns = [ + partial( + u.update, + name=self.name, + key="version", + value=main_screen.version, + ), + partial( + u.update, name=self.name, key="device", value=main_screen.device + ), + partial(u.update, name=self.name, key="clear"), + partial(u.update, name=self.name, key="flash-button"), + partial(u.update, name=self.name, key="airgap-button"), + ] + + for fn in fns: + Clock.schedule_once(fn, 0) + + self.set_screen(name="UnzipStableScreen", direction="left") + + elif args[1] == "MainScreen": + self.set_screen(name="MainScreen", direction="right") + + setattr(VerifyStableZipScreen, f"on_ref_press_{self.id}", _on_ref_press) + + fn = partial(self.update, name=self.name, key="canvas") + Clock.schedule_once(fn) + + def update(self, *args, **kwargs): + """Update widget from other screens""" + + name = kwargs.get("name") + key = kwargs.get("key") + value = kwargs.get("value") + + # Check if update to screen + if name in ("ConfigKruxInstaller", "VerifyStableZipScreen"): + self.debug(f"Updating {self.name} from {name}...") + else: + self.redirect_error(f"Invalid screen name: {name}") + return + + # Check locale + if key == "locale": + if value is not None: + self.locale = value + else: + self.redirect_error(f"Invalid value for key '{key}': '{value}'") + + elif key == "canvas": + with self.canvas.before: + Color(0, 0, 0, 1) + Rectangle(size=(Window.width, Window.height)) + + else: + self.redirect_error(f'Invalid key: "{key}"') + + def on_pre_enter(self): + self.ids[f"{self.id}_grid"].clear_widgets() + verifying_msg = self.translate("Verifying integrity and authenticity") + + warning = Label( + text="".join( + [ + f"[size={self.SIZE_MM}sp]", + "[color=#efcc00]", + verifying_msg, + "[/color]", + "[/size]", + ] + ), + markup=True, + valign="center", + halign="left", + ) + warning.id = f"{self.id}_label" + self.ids[f"{self.id}_grid"].add_widget(warning) + self.ids[warning.id] = WeakProxy(warning) + self.ids[warning.id].bind( + on_ref_press=getattr(VerifyStableZipScreen, f"on_ref_press_{self.id}") + ) + + def on_enter(self): + assets_dir = VerifyStableZipScreen.get_destdir_assets() + main_screen = self.manager.get_screen("MainScreen") + + result_sha256 = self.verify_sha256( + assets_dir=assets_dir, version=main_screen.version + ) + self.ids[f"{self.id}_label"].text = result_sha256 + + result_sign = self.verify_signature( + assets_dir=assets_dir, version=main_screen.version + ) + self.ids[f"{self.id}_label"].text += result_sign + + def verify_sha256(self, assets_dir: str, version: str) -> str: + """Do the verification when entering on screen""" + # verify integrity + sha256_data_0 = Sha256Verifyer(filename=f"{assets_dir}/krux-{version}.zip") + sha256_data_1 = Sha256CheckVerifyer( + filename=f"{assets_dir}/krux-{version}.zip.sha256.txt" + ) + + sha256_data_0.load() + sha256_data_1.load() + txt_hash_0 = sha256_data_0.data.split(" ")[0] + txt_hash_1 = sha256_data_1.data.split(" ")[0] + checksum = sha256_data_0.verify(txt_hash_1) + + # memorize result + self.success = checksum + + filepath = os.path.join(assets_dir, f"krux-{version}.zip") + integrity_msg = self.translate("Integrity verification") + computed_msg = self.translate("computed hash from") + provided_msg = self.translate("provided hash from") + hash_color = "" + hash_msg = "" + + if sys.platform in ("linux", "win32"): + size = [self.SIZE_MP, self.SIZE_P, self.SIZE_PP] + else: + size = [self.SIZE_M, self.SIZE_MP, self.SIZE_P] + + # slice strings, two by two, to better visualization + chunk_sha_0 = [txt_hash_0[i : i + 2] for i in range(0, len(txt_hash_0), 2)] + chunk_sha_1 = [txt_hash_1[i : i + 2] for i in range(0, len(txt_hash_1), 2)] + + # if strings is greater than 16, split in a 2 subsets + subset_sha_0 = [ + " ".join(chunk_sha_0[i : i + 16]) for i in range(0, len(chunk_sha_0), 16) + ] + subset_sha_1 = [ + " ".join(chunk_sha_1[i : i + 16]) for i in range(0, len(chunk_sha_1), 16) + ] + + # join the 2 subsets with \n (next line) string + sha_0 = "\n".join(subset_sha_0) + sha_1 = "\n".join(subset_sha_1) + + if checksum: + hash_color = "#00FF00" + hash_msg = self.translate("SUCCESS") + else: + hash_color = "FF0000" + hash_msg = self.translate("FAILED") + + return "".join( + [ + f"[size={size[0]}sp]", + f"[u]{integrity_msg.upper()}[/u]: ", + f"[b][color={hash_color}]{hash_msg}[/color][/b]", + "[/size]", + "\n", + "\n", + f"[size={size[1]}sp]", + f"[b]{computed_msg} [color=#777777]{filepath}[/color][/b]", + "[/size]", + "\n", + f"[size={size[1]}sp]{sha_0}[/size]", + "\n", + "\n", + f"[size={size[1]}sp]", + f"[b]{provided_msg} [color=#777777]{filepath}.sha256.txt[/color][/b]", + "[/size]", + "\n", + f"[size={size[1]}sp]{sha_1}[/size]", + "\n", + "\n", + "\n", + ] + ) + + def verify_signature(self, assets_dir: str, version: str) -> bool | str: + # verify signature + signature = SigCheckVerifyer(filename=f"{assets_dir}/krux-{version}.zip.sig") + publickey = PemCheckVerifyer(filename=f"{assets_dir}/selfcustody.pem") + signature.load() + publickey.load() + sig_verifyer = SigVerifyer( + filename=f"{assets_dir}/krux-{version}.zip", + regexp=r"^.*\.zip$", + signature=signature.data, + pubkey=publickey.data, + ) + sig_verifyer.load() + checksig = sig_verifyer.verify() + + # memorize result + self.success = self.success and checksig + + authenticity_msg = self.translate("Authenticity verification") + good_msg = self.translate("GOOD") + bad_msg = self.translate("BAD") + sig_msg = self.translate("SIGNATURE") + installed_msg = self.translate("If you have openssl installed on your system") + check_msg = self.translate("you can check manually with the following command") + proceed = self.translate("Proceed") + back = self.translate("Back") + filepath = os.path.join(assets_dir, f"krux-{version}.zip") + pempath = os.path.join(assets_dir, "selfcustody.pem") + + if sys.platform in ("linux", "win32"): + size = [self.SIZE_MP, self.SIZE_P] + else: + size = [self.SIZE_M, self.SIZE_MP] + + if checksig: + sig_color = "#00FF00" + res_msg = good_msg + else: + sig_color = "FF0000" + res_msg = bad_msg + + return "".join( + [ + f"[size={size[0]}sp]", + f"[u]{authenticity_msg.upper()}[/u]: ", + f"[b][color={sig_color}]{res_msg} {sig_msg}[/color][/b]", + "[/size]", + "\n", + "\n", + "\n", + f"[size={size[1]}sp]{installed_msg}[/size]", + "\n" f"[size={size[1]}sp]{check_msg}:[/size]", + "\n", + "\n", + f"[size={size[1]}sp]", + "[b]", + f"openssl sha256< [color=#777777]{filepath}[/color] -binary | \\", + "\n" + f"openssl pkeyutl -verify -pubin -inkey [color=#777777]{pempath}[/color] \\", + "\n", + f"-sigfile [color=#777777]{filepath}.sig[/color]", + "[/size]", + "[/b]", + "\n", + "\n", + f"[size={size[1]}sp][b][color=#{sig_color}]{res_msg} {sig_msg}[/b][/color][/size]", + "\n", + "\n", + f"[size={size[0]}sp]", + f"[ref=UnzipStableScreen][color=#00ff00][u]{proceed}[/u][/ref][/color]", + " ", + f"[ref=MainScreen][color=#ff0000][u]{back}[/u][/ref][/color]", + "[/b]", + "[/size]", + ] + ) diff --git a/src/app/screens/warning_already_downloaded_screen.py b/src/app/screens/warning_already_downloaded_screen.py new file mode 100644 index 00000000..2a19eb42 --- /dev/null +++ b/src/app/screens/warning_already_downloaded_screen.py @@ -0,0 +1,165 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +about_screen.py +""" +import sys +from functools import partial +from kivy.clock import Clock +from kivy.graphics.vertex_instructions import Rectangle +from kivy.graphics.context_instructions import Color +from kivy.core.window import Window +from kivy.weakproxy import WeakProxy +from kivy.uix.label import Label +from kivy.uix.stacklayout import StackLayout +from kivy.uix.button import Button +from src.utils.constants import get_name, get_version +from src.app.screens.base_screen import BaseScreen +from src.i18n import T + + +class WarningAlreadyDownloadedScreen(BaseScreen): + """WarningAlreadyDownloadedScreen warns user about an asset that is already downloaded""" + + def __init__(self, **kwargs): + super().__init__( + wid="warning_already_downloaded_screen", + name="WarningAlreadyDownloadedScreen", + **kwargs, + ) + + self.make_grid(wid=f"{self.id}_grid", rows=2) + + self.make_image( + wid=f"{self.id}_loader", + source=self.warn_img, + root_widget=f"{self.id}_grid", + ) + + self.make_label( + wid=f"{self.id}_label", + text="", + root_widget=f"{self.id}_grid", + markup=True, + halign="justify", + ) + + def _on_ref_press(*args): + if args[1] == "DownloadStableZipScreen": + main_screen = self.manager.get_screen("MainScreen") + download_screen = self.manager.get_screen("DownloadStableZipScreen") + fn = partial( + download_screen.update, + name=self.name, + key="version", + value=main_screen.version, + ) + Clock.schedule_once(fn, 0) + self.set_screen(name="DownloadStableZipScreen", direction="left") + + if args[1] == "VerifyStableZipScreen": + self.set_screen(name="VerifyStableZipScreen", direction="left") + + # When [ref] markup text is clicked, do a action like a button + setattr( + WarningAlreadyDownloadedScreen, f"on_ref_press_{self.id}", _on_ref_press + ) + self.ids[f"{self.id}_label"].bind(on_ref_press=_on_ref_press) + + fn = partial(self.update, name=self.name, key="canvas") + Clock.schedule_once(fn, 0) + + def update(self, *args, **kwargs): + """Update buttons on related screen""" + name = kwargs.get("name") + key = kwargs.get("key") + value = kwargs.get("value") + + if name in ( + "ConfigKruxInstaller", + "MainScreen", + "WarningAlreadyDownloadedScreen", + ): + self.debug(f"Updating {self.name} from {name}") + else: + self.redirect_error(f"Invalid screen name: {name}") + return + + # Check locale + if key == "locale": + if value is not None: + self.locale = value + else: + self.redirect_error(f"Invalid value for key '{key}': '{value}'") + + elif key == "canvas": + # prepare background + with self.canvas.before: + Color(0, 0, 0, 1) + Rectangle(size=(Window.width, Window.height)) + + elif key == "version": + warning_msg = self.translate("Assets already downloaded") + ask_proceed = self.translate( + "Do you want to proceed with the same file or do you want to download it again?" + ) + download_msg = self.translate("Download again") + proceed_msg = self.translate("Proceed with current file") + + if sys.platform in ("linux", "win32"): + size = [self.SIZE_M, self.SIZE_MP, self.SIZE_P] + + else: + size = [self.SIZE_MM, self.SIZE_MP, self.SIZE_MP] + + self.ids[f"{self.id}_label"].text = "".join( + [ + f"[size={size[0]}sp][b]{warning_msg}[/b][/size]", + "\n", + f"[size={size[2]}sp]* krux-{value}.zip[/size]", + "\n", + f"[size={size[2]}sp]* krux-{value}.zip.sha256.txt[/size]", + "\n", + f"[size={size[2]}sp]* krux-{value}.zip.sig[/size]", + "\n", + f"[size={size[2]}sp]* selfcustody.pem[/size]", + "\n", + "\n", + f"[size={size[1]}sp]{ask_proceed}[/size]", + "\n", + "\n", + f"[size={size[0]}]" f"[color=#00ff00]", + "[ref=DownloadStableZipScreen]", + f"[u]{download_msg}[/u]", + "[/ref]", + "[/color]", + " ", + "[color=#efcc00]", + "[ref=VerifyStableZipScreen]", + f"[u]{proceed_msg}[/u]", + "[/ref]", + "[/color]", + "[/size]", + ] + ) + + else: + self.redirect_error(f'Invalid key: "{key}"') diff --git a/src/app/screens/warning_beta_screen.py b/src/app/screens/warning_beta_screen.py new file mode 100644 index 00000000..af105d57 --- /dev/null +++ b/src/app/screens/warning_beta_screen.py @@ -0,0 +1,153 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +about_screen.py +""" + +from src.utils.constants import get_name, get_version +from src.app.screens.base_screen import BaseScreen +from src.i18n import T + + +class WarningBetaScreen(BaseScreen): + """WarningBetaScreen warns user about krux beta versions""" + + def __init__(self, **kwargs): + super().__init__(wid="warning_beta_screen", name="WarningBetaScreen", **kwargs) + self.make_grid(wid="warning_beta_screen_grid", rows=2) + + warning = self.translate("WARNING") + test_repo = self.translate("This is our test repository") + unsg_bin = self.translate( + "These are unsigned binaries for the latest and most experimental features" + ) + just_try = self.translate( + "and it's just for trying new things and providing feedback." + ) + + proceed = self.translate("Proceed") + back = self.translate("Back") + + text = "".join( + [ + f"[size={self.SIZE_MM}sp]", + "[color=#efcc00]", + f"[b]{warning}[/b]", + "[/color]", + "[/size]", + "\n", + "\n", + f"[size={self.SIZE_M}sp]", + f"[color=#efcc00]{test_repo}[/color]", + "[/size]", + "\n", + f"[size={self.SIZE_MP}sp]{unsg_bin}[/size]", + "\n", + f"[size={self.SIZE_MP}sp]{just_try}[/size]", + "\n", + "\n", + f"[size={self.SIZE_MM}sp]", + "[color=#00ff00]", + f"[u]{proceed}[/u]", + "[/color]", + " ", + "[color=#ff0000]", + f"[u]{back}[/u]", + "[/color]", + "[/size]", + ] + ) + + # START of on_press buttons + def _press(instance): + self.debug(f"Calling Button::{instance.id}::on_press") + self.set_background(wid=instance.id, rgba=(0.25, 0.25, 0.25, 1)) + + # END of on_press buttons + + # START of on_release_buttons + def _release(instance): + self.debug(f"Calling Button::{instance.id}::on_release") + self.set_background(wid=instance.id, rgba=(0, 0, 0, 1)) + self.set_screen(name="MainScreen", direction="right") + + self.make_button( + row=0, + id="warning_beta_screen_warn", + root_widget="warning_beta_screen_grid", + text=text, + markup=True, + on_press=_press, + on_release=_release, + ) + + def update(self, *args, **kwargs): + """Update buttons on related screen""" + name = kwargs.get("name") + key = kwargs.get("key") + value = kwargs.get("value") + + # Check if update to screen + if name in ("ConfigKruxInstaller",): + self.debug(f"Updating {self.name} from {name}...") + else: + self.redirect_error(f"Invalid screen name: {name}") + + # Check locale + if key == "locale": + if value is not None: + self.locale = value + warning = self.translate("WARNING") + test_repo = self.translate("This is our test repository") + unsg_bin = self.translate( + "These are unsigned binaries for the latest and most experimental features" + ) + just_try = self.translate( + "and it's just for trying new things and providing feedback." + ) + proceed = self.translate("Proceed") + back = self.translate("Back") + + text = "".join( + [ + f"[size={self.SIZE_MM}sp][color=#efcc00][b]{warning}[/b][/color][/size]", + "\n", + "\n", + f"[size={self.SIZE_M}sp][color=#efcc00]{test_repo}[/color][/size]", + "\n", + f"[size={self.SIZE_MP}sp]{unsg_bin}[/size]", + "\n", + f"[size={self.SIZE_MP}sp]{just_try}[/size]", + "\n", + "\n", + f"[size={self.SIZE_MM}sp]", + f"[color=#00ff00]{proceed}[/color] [color=#ff0000]{back}[/color]", + "[/size]", + ] + ) + + self.ids["warning_beta_screen_warn"].text = text + + else: + self.redirect_error(f"Invalid value for key {key}: {value}") + + else: + self.redirect_error(f'Invalid key: "{key}"') diff --git a/src/app/screens/warning_wipe_screen.py b/src/app/screens/warning_wipe_screen.py new file mode 100644 index 00000000..538bd34b --- /dev/null +++ b/src/app/screens/warning_wipe_screen.py @@ -0,0 +1,186 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +about_screen.py +""" +import sys +from functools import partial +from kivy.clock import Clock +from kivy.graphics.vertex_instructions import Rectangle +from kivy.graphics.context_instructions import Color +from kivy.core.window import Window +from kivy.weakproxy import WeakProxy +from kivy.uix.label import Label +from kivy.uix.stacklayout import StackLayout +from kivy.uix.button import Button +from src.app.screens import main_screen +from src.utils.constants import get_name, get_version +from src.app.screens.base_screen import BaseScreen + + +class WarningWipeScreen(BaseScreen): + """WarningWipeScreen warns user about the wipe procedure""" + + def __init__(self, **kwargs): + super().__init__( + wid="warning_wipe_screen", + name="WarningWipeScreen", + **kwargs, + ) + + self.make_grid(wid=f"{self.id}_grid", rows=2) + + self.make_image( + wid=f"{self.id}_loader", + source=self.warn_img, + root_widget=f"{self.id}_grid", + ) + + self.make_label( + wid=f"{self.id}_label", + text=self.make_label_text(), + root_widget=f"{self.id}_grid", + markup=True, + halign="justify", + ) + + def _on_ref_press(*args): + if args[1] == "WipeScreen": + partials = [] + main_screen = self.manager.get_screen("MainScreen") + wipe_screen = self.manager.get_screen(args[1]) + baudrate = WarningWipeScreen.get_baudrate() + partials.append( + partial( + wipe_screen.update, + name=self.name, + key="device", + value=main_screen.device, + ) + ) + partials.append( + partial( + wipe_screen.update, + name=self.name, + key="wiper", + value=baudrate, + ) + ) + + for fn in partials: + Clock.schedule_once(fn, 0) + + self.set_screen(name=args[1], direction="left") + + if args[1] == "MainScreen": + self.set_screen(name="MainScreen", direction="right") + + # When [ref] markup text is clicked, do a action like a button + setattr(WarningWipeScreen, f"on_ref_press_{self.id}", _on_ref_press) + self.ids[f"{self.id}_label"].bind(on_ref_press=_on_ref_press) + + fn = partial(self.update, name=self.name, key="canvas") + Clock.schedule_once(fn, 0) + + def on_enter(self): + self.ids[f"{self.id}_label"].text = self.make_label_text() + + def update(self, *args, **kwargs): + """Update buttons on related screen""" + name = kwargs.get("name") + key = kwargs.get("key") + value = kwargs.get("value") + + if name in ( + "ConfigKruxInstaller", + "MainScreen", + "WarningWipeScreen", + ): + self.debug(f"Updating {self.name} from {name}") + else: + self.redirect_error(f"Invalid screen name: {name}") + return + + # Check locale + if key == "locale": + if value is not None: + self.locale = value + self.ids[f"{self.id}_label"].text = self.make_label_text() + else: + self.redirect_error(f"Invalid value for key '{key}': '{value}'") + + elif key == "canvas": + # prepare background + with self.canvas.before: + Color(0, 0, 0, 1) + Rectangle(size=(Window.width, Window.height)) + + else: + self.redirect_error(f'Invalid key: "{key}"') + + def make_label_text(self): + full_wipe = self.translate( + "You are about to initiate a FULL WIPE of this device" + ) + operation = self.translate("This operation will") + erase = self.translate("Permanently erase all saved data") + remove = self.translate("Remove the existing firmware") + render = self.translate( + "Render the device non-functional until new firmware is re-flashed" + ) + proceed = self.translate("Proceed") + back = self.translate("Back") + + if sys.platform in ("linux", "win32"): + sizes = [self.SIZE_MP, self.SIZE_P] + + else: + sizes = [self.SIZE_MM, self.SIZE_M] + + return "".join( + [ + "[color=#EFCC00]", + f"[size={sizes[0]}]", + full_wipe, + "[/size]", + "[/color]" "\n", + "\n", + f"[size={sizes[1]}]", + f"{operation}:", + "\n", + f"* {erase}", + "\n", + f"* {remove}", + "\n", + f"* {render}", + "[/size]", + "\n", + "\n", + f"[size={sizes[0]}]" "[color=#00FF00]", + f"[ref=WipeScreen][u]{proceed}[/u][/ref]", + "[/color]", + " ", + "[color=#FF0000]", + f"[ref=MainScreen][u]{back}[/u][/ref]", + "[/color]", + "[/size]", + ] + ) diff --git a/src/app/screens/wipe_screen.py b/src/app/screens/wipe_screen.py new file mode 100644 index 00000000..0a029776 --- /dev/null +++ b/src/app/screens/wipe_screen.py @@ -0,0 +1,272 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +wipe_screen.py +""" +import sys +import threading +import traceback +from functools import partial +from kivy.clock import Clock +from kivy.app import App +from kivy.core.window import Window +from kivy.graphics.vertex_instructions import Rectangle +from kivy.graphics.context_instructions import Color +from src.utils.flasher.wiper import Wiper +from src.app.screens.base_flash_screen import BaseFlashScreen + + +class WipeScreen(BaseFlashScreen): + """Flash screen is where flash occurs""" + + def __init__(self, **kwargs): + super().__init__(wid="wipe_screen", name="WipeScreen", **kwargs) + self.wiper = None + self.success = False + fn = partial(self.update, name=self.name, key="canvas") + Clock.schedule_once(fn, 0) + + def on_pre_enter(self): + self.ids[f"{self.id}_grid"].clear_widgets() + + def on_print_callback(*args, **kwargs): + text = " ".join(str(x) for x in args) + self.info(text) + + text = text.replace( + "\x1b[32m\x1b[1m[INFO]\x1b[0m", "[color=#00ff00]INFO[/color]" + ) + text = text.replace( + "\x1b[33mISP loaded", "[color=#efcc00]ISP loaded[/color]" + ) + text = text.replace( + "\x1b[33mInitialize K210 SPI Flash", + "[color=#efcc00]Initialize K210 SPI Flash[/color]", + ) + text = text.replace("Flash ID: \x1b[33m", "Flash ID: [color=#efcc00]") + text = text.replace( + "\x1b[0m, unique ID: \x1b[33m", "[/color], unique ID: [color=#efcc00]" + ) + text = text.replace("\x1b[0m, size: \x1b[33m", "[/color], size: ") + text = text.replace("\x1b[0m MB", "[/color] MB") + text = text.replace("\x1b[0m", "") + text = text.replace("\x1b[33m", "") + text = text.replace( + "[INFO] Erasing the whole SPI Flash", + "[color=#00ff00]INFO[/color][color=#efcc00] Erasing the whole SPI Flash [/color]", + ) + + text = text.replace( + "\x1b[31m\x1b[1m[ERROR]\x1b[0m", "[color=#ff0000]ERROR[/color]" + ) + + self.output.append(text) + + if len(self.output) > 18: + del self.output[:1] + + if "SPI Flash erased." in text: + self.trigger() + + self.ids[f"{self.id}_info"].text = "\n".join(self.output) + + def on_ref_press(*args): + if args[1] == "Back": + self.set_screen(name="MainScreen", direction="right") + + elif args[1] == "Quit": + App.get_running_app().stop() + + else: + self.redirect_error(f"Invalid ref: {args[1]}") + + def on_trigger_callback(dt): + self.success = True + del self.output[4:] + self.ids[f"{self.id}_loader"].source = self.done_img + self.ids[f"{self.id}_loader"].reload() + done = self.translate("DONE") + back = self.translate("Back") + quit = self.translate("Quit") + if sys.platform in ("linux", "win32"): + size = self.SIZE_M + else: + size = self.SIZE_M + + self.ids[f"{self.id}_progress"].text = "".join( + [ + f"[size={size}sp][b]{done}![/b][/size]", + "\n", + f"[size={size}sp]", + "[color=#00FF00]", + f"[ref=Back][u]{back}[/u][/ref]", + "[/color]", + " ", + "[color=#EFCC00]", + f"[ref=Quit][u]{quit}[/u][/ref]", + "[/color]", + ] + ) + self.ids[f"{self.id}_progress"].bind(on_ref_press=on_ref_press) + + setattr(WipeScreen, "on_print_callback", on_print_callback) + setattr(WipeScreen, "on_trigger_callback", on_trigger_callback) + + self.make_subgrid( + wid=f"{self.id}_subgrid", rows=3, root_widget=f"{self.id}_grid" + ) + + self.make_image( + wid=f"{self.id}_loader", + source=self.load_img, + root_widget=f"{self.id}_subgrid", + ) + + self.make_label( + wid=f"{self.id}_progress", + text="", + root_widget=f"{self.id}_subgrid", + markup=True, + halign="center", + ) + + self.make_label( + wid=f"{self.id}_info", + text="", + root_widget=f"{self.id}_grid", + markup=True, + halign="justify", + ) + + def on_enter(self): + """ + Event fired when the screen is displayed and the entering animation is complete. + """ + self.debug("Staring wipe...") + if self.wiper is not None: + please = self.translate("PLEASE DO NOT UNPLUG YOUR DEVICE") + if sys.platform in ("linux", "win32"): + sizes = [self.SIZE_M, self.SIZE_PP] + else: + sizes = [self.SIZE_MM, self.SIZE_MP] + + self.ids[f"{self.id}_progress"].text = f"[size={sizes[0]}sp][b]{please}[/b]" + self.output = [] + self.progress = "" + self.is_done = False + self.trigger = getattr(self.__class__, "on_trigger_callback") + self.wiper.ktool.__class__.print_callback = getattr( + self.__class__, "on_print_callback" + ) + on_process_callback = partial(self.wiper.wipe, device=self.device) + self.thread = threading.Thread(name=self.name, target=on_process_callback) + + # if anything wrong happen, show it + def hook(err): + msg = "".join( + traceback.format_exception( + err.exc_type, err.exc_value, err.exc_traceback + ) + ) + self.error(msg) + done = self.translate("DONE") + back = self.translate("Back") + quit = self.translate("Quit") + + self.ids[f"{self.id}_progress"].text = "".join( + [ + f"[size={sizes[0]}]", + f"[color=#FF0000]{"Wipe failed" if not self.success else done}[/color]", + "[/size]", + "\n", + "\n", + f"[size={sizes[0]}]" "[color=#00FF00]", + f"[ref=Back][u]{back}[/u][/ref]", + "[/color]", + " ", + "[color=#EFCC00]", + f"[ref=Quit][u]{quit}[/u][/ref]", + "[/color]", + "[/size]", + ] + ) + + self.ids[f"{self.id}_info"].text = "".join( + [ + f"[size={sizes[1]}]", + msg, + "[/size]", + ] + ) + + # hook what happened + threading.excepthook = hook + self.thread.start() + else: + self.redirect_error("Wiper isnt configured") + + def update(self, *args, **kwargs): + """Update screen with firmware key. Should be called before `on_enter`""" + name = kwargs.get("name") + key = kwargs.get("key") + value = kwargs.get("value") + + if name in ( + "ConfigKruxInstaller", + "MainScreen", + "WarningWipeScreen", + "WipeScreen", + ): + self.debug(f"Updating {self.name} from {name}...") + else: + self.redirect_error(f"Invalid screen name: {name}") + return + + key = kwargs.get("key") + value = kwargs.get("value") + + if key == "locale": + if value is not None: + self.locale = value + else: + self.redirect_error(f"Invalid value for key '{key}': {value}") + + elif key == "canvas": + # prepare background + with self.canvas.before: + Color(0, 0, 0, 1) + Rectangle(size=(Window.width, Window.height)) + + elif key == "device": + if value is not None: + self.device = value + else: + self.redirect_error(f"Invalid value for key '{key}': {value}") + + elif key == "wiper": + if value is not None: + self.wiper = Wiper() + self.wiper.baudrate = value + else: + self.redirect_error(f"Invalid value for key '{key}': {value}") + + else: + raise ValueError(f'Invalid key: "{key}"') diff --git a/src/assets/electron.svg b/src/assets/electron.svg deleted file mode 100644 index 1c5cccbe..00000000 --- a/src/assets/electron.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/electron-env.d.ts b/src/electron-env.d.ts deleted file mode 100644 index d7637b6a..00000000 --- a/src/electron-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// src/electron-env.d.ts(need create) -export { } -declare global { - interface Window { - // Expose some Api through preload script - api?: any; - ipcRenderer: import('electron').IpcRenderer - } -} diff --git a/src/i18n/__init__.py b/src/i18n/__init__.py new file mode 100644 index 00000000..ce9f85a1 --- /dev/null +++ b/src/i18n/__init__.py @@ -0,0 +1,66 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +__init__.py +""" +import re +import os +import json +from easy_i18n.t import Ai18n +from src.utils.trigger import Trigger + +I18N_DIRNAME = os.path.dirname(os.path.realpath(__file__)) +I18N_LOCALES = [] +I18N_FILES = [] + +for f in os.listdir(I18N_DIRNAME): + i18n_file = os.path.join(I18N_DIRNAME, f) + if os.path.isfile(i18n_file): + if re.findall(r"^[a-z]+\_[A-Z]+\.UTF-8\.json$", f): + _locale = f.split(".json") + I18N_LOCALES.append({"name": _locale[0], "file": i18n_file}) + +a_i18n = Ai18n() +a_i18n.locales = [name for name, file in I18N_LOCALES] + +for _locale in I18N_LOCALES: + _name = _locale["name"] + _file = _locale["file"] + with open(_file, mode="r", encoding="utf-8") as f: + data = json.loads(f.read()) + for screen, value in data.items(): + for word, translation in value.items(): + a_i18n.add(k=word, message=translation, module=screen, locale=_name) + + +# pylint: disable=invalid-name +def T(msg: str, locale: str, module: str): + """Check if a translation exist and if exist, tranlsate it""" + found = False + for loc in I18N_LOCALES: + name = loc["name"] + if name == locale: + found = True + + if not found: + raise ValueError(f"Locale '{locale}' not found in translations") + + return a_i18n.translate(msg, locale=locale, module=module) diff --git a/src/i18n/af_ZA.UTF-8.json b/src/i18n/af_ZA.UTF-8.json new file mode 100644 index 00000000..a359f64a --- /dev/null +++ b/src/i18n/af_ZA.UTF-8.json @@ -0,0 +1,134 @@ +{ + "check_permissions_screen": { + "Setup": "Stel", + "for": "vir", + "Checking": "Kontroleer", + "permissions for": "groeptoestemmings vir", + "WARNING": "WAARSKUWING", + "This is the first run of KruxInstaller in": "Dit is die eerste lopie van KruxInstaller in", + "and it appears that you do not have privileged access to make flash procedures": "en dit blyk dat u nie bevoorregte toegang het om flitsprosedures te maak nie", + "To proceed, click in the screen and a prompt will ask for your password": "Om voort te gaan, klik op die Laat toe-knoppie en 'n opdrag vra u wagwoord", + "to execute the following command": "om die volgende opdrag uit te voer", + "You may need to logout (or even reboot)": "Miskien moet u afmeld (of selfs weer begin)", + "and back in for the new group to take effect": "en terug vir die nuwe groep om in werking te tree", + "Do not worry, this message won't appear again": "Moenie bekommerd wees nie, hierdie boodskap sal nie weer verskyn nie", + "Allow": "Laat", + "Deny": "Weier" + }, + "check_internet_connection_screen": { + "Checking your internet connection": "Kontroleer jou internetverbinding" + }, + "main_screen": { + "Version": "Weergawe", + "Device": "Toestel", + "select a new one": "kies 'n nuwe een", + "Flash": "Programmeer die firmware", + "Wipe": "Skakel toestel af", + "Settings": "Instellings", + "About": "Oor", + "Fetching data from": "Haal tans data van" + }, + "select_version_screen": { + "Old versions": "Ou weergawes", + "Back": "Terug" + }, + "select_old_version_screen": { + "Back": "Terug" + }, + "warning_beta_screen": { + "WARNING": "WAARSKUWING", + "This is our test repository": "Dit is ons toetsbewaarplek", + "These are unsigned binaries for the latest and most experimental features": "Dit is ongetekende binaries vir die nuutste en mees eksperimentele kenmerke", + "and it's just for trying new things and providing feedback.": "en dit is net om nuwe dinge te probeer en terugvoer te gee.", + "Proceed": "Voortgaan", + "Back": "Terug" + }, + "download_beta_screen": { + "Connecting": "Koppel tans", + "Downloading": "Laai", + "to": "af na", + "of": "van", + "downloaded": "afgelaai" + }, + "warning_already_downloaded_screen": { + "Assets already downloaded": "Bates wat reeds afgelaai is", + "Do you want to proceed with the same file or do you want to download it again?": "Wil jy voortgaan met dieselfde lêer of wil jy dit weer aflaai?", + "Download again": "Laai weer af", + "Proceed with current file": "Gaan voort met die huidige lêer" + }, + "download_stable_zip_screen": { + "Connecting": "Koppel tans", + "Downloading": "Laai", + "to": "af na", + "of": "van", + "downloaded": "afgelaai" + }, + "download_stable_zip_sha256_screen": { + "Connecting": "Koppel tans", + "Downloading": "Laai", + "to": "af na", + "of": "van", + "downloaded": "afgelaai" + }, + "download_stable_zip_sig_screen": { + "Connecting": "Koppel tans", + "Downloading": "Laai", + "to": "af na", + "of": "van", + "downloaded": "afgelaai" + }, + "download_selfcustody_pem_screen": { + "Connecting": "Koppel tans", + "Downloading": "Laai", + "to": "af na", + "downloaded": "afgelaai" + }, + "verify_stable_zip_screen": { + "Verifying integrity and authenticity": "Verifieer integriteit en egtheid", + "Integrity verification": "Integriteit verifikasie", + "computed hash from": "berekende hash van", + "provided hash from": "verskaf hash van", + "SUCCESS": "SUKSES", + "FAILED": "HET MISLUK", + "Authenticity verification": "Verifikasie van egtheid", + "GOOD": "GOEIE", + "BAD": "SLEGTE", + "SIGNATURE": "HANDTEKENING", + "If you have openssl installed on your system": "As u openssl op u stelsel geïnstalleer het", + "you can check manually with the following command": "U kan handmatig met die volgende opdrag kyk", + "Proceed": "Voortgaan", + "Back": "Terug" + }, + "unzip_stable_screen": { + "Flash with": "Flits met", + "Air-gapped update with": "Air-gapped opdatering met (gou)", + "Unziping": "Uitpak", + "Unziped": "Uitgepak" + }, + "flash_screen": { + "DONE": "GEDOEN", + "Back": "Terug", + "Quit": "Sluit", + "Flashing": "Doen flash", + "at": "op" + }, + "wipe_screen": { + "PLEASE DO NOT UNPLUG YOUR DEVICE": "MOET ASSEBLIEF NIE U TOESTEL ONTKOPPEL NIE", + "DONE": "GEDOEN", + "Back": "Terug", + "Quit": "Sluit" + }, + "warning_wipe_screen": { + "You are about to initiate a FULL WIPE of this device": "U is op die punt om 'n VOLLEDIGE WIPE van hierdie toestel te begin", + "This operation will": "Hierdie operasie sal", + "Permanently erase all saved data": "Vee alle gestoorde data permanent uit", + "Remove the existing firmware": "Verwyder die bestaande firmware", + "Render the device non-functional until new firmware is re-flashed": "Maak die toestel nie funksioneel totdat nuwe firmware weer geflits is nie", + "Proceed": "Voortgaan", + "Back": "Terug" + }, + "about_screen": { + "follow us on X": "volg ons op X", + "Back": "Terug" + } +} diff --git a/src/i18n/en_US.UTF-8.json b/src/i18n/en_US.UTF-8.json new file mode 100644 index 00000000..841f406b --- /dev/null +++ b/src/i18n/en_US.UTF-8.json @@ -0,0 +1,135 @@ +{ + "check_permissions_screen": { + "Setup": "Setup", + "for": "for", + "Checking": "Checking", + "permissions for": "permissions for", + "WARNING": "WARNING", + "This is the first run of KruxInstaller in": "This is the first run of KruxInstaller in", + "and it appears that you do not have privileged access to make flash procedures": "and it appears that you do not have privileged access to make flash procedures", + "To proceed, click in the Allow button and a prompt will ask for your password": "To proceed, click in the Allow button and a prompt will ask for your password", + "to execute the following command": "to execute the following command", + "You may need to logout (or even reboot)": "You may need to logout (or even reboot)", + "and back in for the new group to take effect": "and back in for the new group to take effect", + "Do not worry, this message won't appear again": "Do not worry, this message won't appear again", + "Allow": "Allow", + "Deny": "Deny" + }, + "check_internet_connection_screen": { + "Checking your internet connection": "Checking your internet connection" + }, + "main_screen": { + "Version": "Version", + "Device": "Device", + "select a new one": "select a new one", + "Flash": "Flash firmware", + "Wipe": "Wipe device", + "Settings": "Settings", + "About": "About", + "Fetching data from": "Fetching data from" + }, + "select_version_screen": { + "Old versions": "Old versions", + "Back": "Back" + }, + "select_old_version_screen": { + "Back": "Back" + }, + "warning_beta_screen": { + "WARNING": "WARNING", + "This is our test repository": "This is our test repository", + "These are unsigned binaries for the latest and most experimental features": "These are unsigned binaries for the latest and most experimental features", + "and it's just for trying new things and providing feedback.": "and it's just for trying new things and providing feedback.", + "Proceed": "Proceed", + "Back": "Back" + }, + "download_beta_screen": { + "Connecting": "Connecting", + "Downloading": "Downloading", + "to": "to", + "of": "of", + "downloaded": "downloaded" + }, + "warning_already_downloaded_screen": { + "Assets already downloaded": "Assets already downloaded", + "Do you want to proceed with the same file or do you want to download it again?": "Do you want to proceed with the same file or do you want to download it again?", + "Download again": "Download again", + "Proceed with current file": "Proceed with current file" + }, + "download_stable_zip_screen": { + "Connecting": "Connecting", + "Downloading": "Downloading", + "to": "to", + "of": "of", + "downloaded": "downloaded" + }, + "download_stable_zip_sha256_screen": { + "Connecting": "Connecting", + "Downloading": "Downloading", + "to": "to", + "of": "of", + "downloaded": "downloaded" + }, + "download_stable_zip_sig_screen": { + "Connecting": "Connecting", + "Downloading": "Downloading", + "to": "to", + "of": "of", + "downloaded": "downloaded" + }, + "download_selfcustody_pem_screen": { + "Connecting": "Connecting", + "Downloading": "Downloading", + "to": "to", + "of": "of", + "downloaded": "downloaded" + }, + "verify_stable_zip_screen": { + "Verifying integrity and authenticity": "Verifying integrity and authenticity", + "Integrity verification": "Integrity verification", + "computed hash from": "computed hash from", + "provided hash from": "provided hash from", + "SUCCESS": "SUCCESS", + "FAILED": "FAILED", + "Authenticity verification": "Authenticity verification", + "GOOD": "GOOD", + "BAD": "BAD", + "SIGNATURE": "SIGNATURE", + "If you have openssl installed on your system": "If you have openssl installed on your system", + "you can check manually with the following command": "you can check with the following command", + "Proceed": "Proceed", + "Back": "Back" + }, + "unzip_stable_screen": { + "Flash with": "Flash with", + "Air-gapped update with": "Air-gapped update with (soon)", + "Unziping": "Extracting", + "Unziped": "Extracted" + }, + "flash_screen": { + "DONE": "DONE", + "Back": "Back", + "Quit": "Quit", + "Flashing": "Flashing", + "at": "at" + }, + "wipe_screen": { + "PLEASE DO NOT UNPLUG YOUR DEVICE": "PLEASE DO NOT UNPLUG YOUR DEVICE", + "DONE": "DONE", + "Back": "Back", + "Quit": "Quit" + }, + "warning_wipe_screen": { + "You are about to initiate a FULL WIPE of this device": "You are about to initiate a FULL WIPE of this device", + "This operation will": "This operation will", + "Permanently erase all saved data": "Permanently erase all saved data", + "Remove the existing firmware": "Remove the existing firmware", + "Render the device non-functional until new firmware is re-flashed": "Render the device non-functional until new firmware is re-flashed", + "Proceed": "Proceed", + "Back": "Back" + }, + "about_screen": { + "follow us on X": "follow us on X", + "Back": "Back" + } +} diff --git a/src/i18n/es_ES.UTF-8.json b/src/i18n/es_ES.UTF-8.json new file mode 100644 index 00000000..f6030485 --- /dev/null +++ b/src/i18n/es_ES.UTF-8.json @@ -0,0 +1,135 @@ +{ + "check_permissions_screen": { + "Setup": "Configuración", + "for": "para", + "Checking": "Comprobación", + "permissions for": "permissos para", + "WARNING": "ADVERTENCIA", + "This is the first run of KruxInstaller in": "Esta es la primera ejecución de KruxInstaller no", + "and it appears that you do not have privileged access to make flash procedures": "y parece que no tiene acceso privilegiado para realizar procedimientos flash", + "To proceed, click in the Allow button and a prompt will ask for your password": "Para continuar, haga clic en el botón Permitir y un mensaje le pedirá su contraseña", + "to execute the following command": "para ejecutar el siguiente comando", + "You may need to logout (or even reboot)": "Es posible que deba cerrar la sesión (o incluso reiniciar)", + "and back in for the new group to take effect": "y volver de nuevo para que el nuevo grupo entre en vigor", + "Do not worry, this message won't appear again": "No te preocupes, este mensaje no volverá a aparecer", + "Allow": "Permitir", + "Deny": "Negar" + }, + "check_internet_connection_screen": { + "Checking your internet connection": "Comprobando tu conexión a Internet" + }, + "main_screen": { + "Version": "Versión", + "Device": "Dispositivo", + "select a new one": "seleccione uno nuevo", + "Flash": "Flash el firmware", + "Wipe": "Limpiar dispositivo", + "Settings": "Ajustes", + "About": "Acerca de", + "Fetching data from": "Obteniendo datos de" + }, + "select_version_screen": { + "Old versions": "Versiones antiguas", + "Back": "Volver" + }, + "select_old_version_screen": { + "Back": "Volver" + }, + "warning_beta_screen": { + "WARNING": "ADVERTENCIA", + "This is our test repository": "Este es nuestro repositorio de pruebas", + "These are unsigned binaries for the latest and most experimental features": "Estos son binarios sin firmar para las funciones más recientes y experimentales", + "and it's just for trying new things and providing feedback.": "y es sólo para probar cosas nuevas y dar opinión.", + "Proceed": "Proceder", + "Back": "Volver" + }, + "download_beta_screen": { + "Connecting": "Conectando a", + "Downloading": "Descargar", + "to": "a", + "of": "de", + "downloaded": "se descargó" + }, + "warning_already_downloaded_screen": { + "Assets already downloaded": "Recursos ya descargados", + "Do you want to proceed with the same file or do you want to download it again?": "¿Quieres continuar con el mismo archivo o quieres descargarlo nuevamente?", + "Download again": "Descargar de nuevo", + "Proceed with current file": "Continuar con el archivo actual" + }, + "download_stable_zip_screen": { + "Connecting": "Conectando a", + "Downloading": "Descargar", + "to": "a", + "of": "de", + "downloaded": "se descargó" + }, + "download_stable_zip_sha256_screen": { + "Connecting": "Conectando a", + "Downloading": "Descargar", + "to": "a", + "of": "de", + "downloaded": "se descargó" + }, + "download_stable_zip_sig_screen": { + "Connecting": "Conectando a", + "Downloading": "Descargar", + "to": "a", + "of": "de", + "downloaded": "se descargó" + }, + "download_selfcustody_pem_screen": { + "Connecting": "Conectando a", + "Downloading": "Descargar", + "to": "a", + "of": "de", + "downloaded": "se descargó" + }, + "verify_stable_zip_screen": { + "Verifying integrity and authenticity": "Verificación de la integridad y autenticidad", + "Integrity verification": "Verificación de integridad", + "computed hash from": "hash calculado a partir de", + "provided hash from": "hash proporcionado de", + "SUCCESS": "ÉXITO", + "FAILED": "FRACASADO", + "Authenticity verification": "Verificación de autenticidad", + "GOOD": "BUENA", + "BAD": "MALA", + "SIGNATURE": "FIRMA", + "If you have openssl installed on your system": "Si tiene openssl instalado en su sistema", + "you can check manually with the following command": "puede comprobarlo manualmente con el siguiente comando", + "Proceed": "Proceder", + "Back": "Volver" + }, + "unzip_stable_screen": { + "Flash with": "Flash con", + "Air-gapped update with": "Actualización aislada con (en breve)", + "Unziping": "Descomprimir", + "Unziped": "Descomprimido" + }, + "flash_screen": { + "DONE": "HECHO", + "Back": "Volver", + "Quit": "Salir", + "Flashing": "Haciendo flash", + "at": "a" + }, + "wipe_screen": { + "PLEASE DO NOT UNPLUG YOUR DEVICE": "POR FAVOR, NO DESCONECTE SU DISPOSITIVO", + "DONE": "HECHO", + "Back": "Volve", + "Quit": "Salir" + }, + "warning_wipe_screen": { + "You are about to initiate a FULL WIPE of this device": "Está a punto de iniciar un BORRADO COMPLETO de este dispositivo", + "This operation will": "Esta operación", + "Permanently erase all saved data": "Borrar permanentemente todos los datos guardados", + "Remove the existing firmware": "Eliminar el firmware existente", + "Render the device non-functional until new firmware is re-flashed": "Hacer que el dispositivo no funcione hasta que se vuelva a actualizar el nuevo firmware", + "Proceed": "Proceder", + "Back": "Volver" + }, + "about_screen": { + "follow us on X": "síguenos en X", + "Back": "Volver" + } +} diff --git a/src/i18n/fr_FR.UTF-8.json b/src/i18n/fr_FR.UTF-8.json new file mode 100644 index 00000000..7d1f48a8 --- /dev/null +++ b/src/i18n/fr_FR.UTF-8.json @@ -0,0 +1,135 @@ +{ + "check_permissions_screen": { + "Setup": "Configuration de", + "for": "pour", + "Checking": "Vérification", + "permissions for": "des autorisations pour", + "WARNING": "AVERTISSEMENT", + "This is the first run of KruxInstaller in": "Il s'agit de la première exécution de KruxInstaller dans", + "and it appears that you do not have privileged access to make flash procedures": "et il semble que vous n'ayez pas d'accès privilégié pour effectuer des procédures flash", + "To proceed, click in the Allow button and a prompt will ask for your password": "Pour continuer, cliquez sur le bouton Autoriser et une invite vous demandera votre mot de passe", + "to execute the following command": "pour exécuter la commande suivante", + "You may need to logout (or even reboot)": "Vous devrez peut-être vous déconnecter (ou même redémarrer)", + "and back in for the new group to take effect": "et de retour pour que le nouveau groupe prenne effet", + "Do not worry, this message won't appear again": "Ne vous inquiétez pas, ce message n'apparaîtra plus", + "Allow": "Autoriser", + "Deny": "Nier" + }, + "check_internet_connection_screen": { + "Checking your internet connection": "Vérification de votre connexion Internet" + }, + "main_screen": { + "Version": "Version", + "Device": "Appareil", + "select a new one": "sélectionnez-en un nouveau", + "Flash": "Flasher le firmware", + "Wipe": "Effacer toutes données", + "Settings": "Paramètres", + "About": "À propos", + "Fetching data from": "Récupérer les données sur" + }, + "select_version_screen": { + "Old versions": "Versions précédentes", + "Back": "Retour" + }, + "select_old_version_screen": { + "Back": "Retour" + }, + "warning_beta_screen": { + "WARNING": "AVERTISSEMENT", + "This is our test repository": "Ceci est notre référentiel de tests", + "These are unsigned binaries for the latest and most experimental features": "Ce sont des binaires non signés pour les fonctionnalités\nles plus récentes et les plus expérimentales", + "and it's just for trying new things and providing feedback.": "en vue d'essayer de nouvelles choses et fournir des commentaires.", + "Proceed": "Continuez", + "Back": "Retour" + }, + "download_beta_screen": { + "Connecting": "Connexion en cours", + "Downloading": "Teléchargement de", + "to": "dans le", + "of": "de", + "downloaded": "téléchargé" + }, + "warning_already_downloaded_screen": { + "Assets already downloaded": "Fichiers déjà téléchargés", + "Do you want to proceed with the same file or do you want to download it again?": "Voulez-vous continuer avec le même fichier ou souhaitez-vous le télécharger à nouveau?", + "Download again": "Télécharger à nouveau", + "Proceed with current file": "Passer au fichier actuel" + }, + "download_stable_zip_screen": { + "Connecting": "Connexion en cours", + "Downloading": "Teléchargement de", + "to": "dans le", + "of": "de", + "downloaded": "téléchargé" + }, + "download_stable_zip_sha256_screen": { + "Connecting": "Connexion en cours", + "Downloading": "Teléchargement de", + "to": "dans le", + "of": "de", + "downloaded": "téléchargé" + }, + "download_stable_zip_sig_screen": { + "Connecting": "Connexion en cours", + "Downloading": "Teléchargement de", + "to": "dans le", + "of": "de", + "downloaded": "téléchargé" + }, + "download_selfcustody_pem_screen": { + "Connecting": "Connexion en cours", + "Downloading": "Teléchargement de", + "to": "dans le", + "of": "de", + "downloaded": "téléchargé" + }, + "verify_stable_zip_screen": { + "Verifying integrity and authenticity": "Vérification de l'intégrité et de l'authenticité", + "Integrity verification": "Vérification de l'intégrité", + "computed hash from": "hash calculé à partir de", + "provided hash from": "hash fourni à partir de", + "SUCCESS": "SUCCÈS", + "FAILED": "RATÉ", + "Authenticity verification": "Vérification de l'authenticité", + "GOOD": "BONNE", + "BAD": "MAUVAISE", + "SIGNATURE": "SIGNATURE", + "If you have openssl installed on your system": "Si openssl est installé sur votre système", + "you can check manually with the following command": "vous pouvez vérifier manuellement avec la commande suivante", + "Proceed": "Continuer", + "Back": "Retour" + }, + "unzip_stable_screen": { + "Flash with": "Flash avec", + "Air-gapped update with": "Mise à jour isolée avec (bientôt)", + "Unziping": "Décompression de", + "Unziped": "Décompressé" + }, + "flash_screen": { + "DONE": "FAIT", + "Back": "Retour", + "Quit": "Fermer", + "Flashing": "Réalisation du flash", + "at": "à" + }, + "wipe_screen": { + "PLEASE DO NOT UNPLUG YOUR DEVICE": "VEUILLEZ NE PAS DÉBRANCHER VOTRE APPAREIL", + "DONE": "FAIT", + "Back": "Retour", + "Quit": "Fermer" + }, + "warning_wipe_screen": { + "You are about to initiate a FULL WIPE of this device": "Vous êtes sur le point de lancer un EFFACEMENT COMPLET de cet appareil", + "This operation will": "Cette opération fera l'affaire", + "Permanently erase all saved data": "Effacez définitivement toutes les données sauvegardées", + "Remove the existing firmware": "Supprimer le firmware existant", + "Render the device non-functional until new firmware is re-flashed": "Rendez l'appareil non fonctionnel jusqu'à ce que le nouveau firmware soit reflashé.", + "Proceed": "Continuer", + "Back": "Retour" + }, + "about_screen": { + "follow us on X": "suivez-nous sur X", + "Back": "Retour" + } +} diff --git a/src/i18n/it_IT.UTF-8.json b/src/i18n/it_IT.UTF-8.json new file mode 100644 index 00000000..eb8da234 --- /dev/null +++ b/src/i18n/it_IT.UTF-8.json @@ -0,0 +1,135 @@ +{ + "check_permissions_screen": { + "Setup": "Configura", + "for": "per", + "Checking": "Verifica", + "permissions for": "delle autorizzazioni per", + "WARNING": "AVVERTIMENTO", + "This is the first run of KruxInstaller in": "Questa è la prima esecuzione di KruxInstaller in", + "and it appears that you do not have privileged access to make flash procedures": "e sembra che tu non disponga di un accesso privilegiato per effettuare procedure flash", + "To proceed, click in the Allow button and a prompt will ask for your password": "Per procedere, fai clic sul pulsante Consenti e un messaggio ti chiederà la password", + "to execute the following command": "per eseguire il seguente comando", + "You may need to logout (or even reboot)": "Potrebbe essere necessario disconnettersi (o addirittura riavviare)", + "and back in for the new group to take effect": "e tornare di nuovo per l'entrata in vigore del nuovo gruppo", + "Do not worry, this message won't appear again": "Non preoccuparti, questo messaggio non apparirà più", + "Allow": "Consenti", + "Deny": "Negare" + }, + "check_internet_connection_screen": { + "Checking your internet connection": "Controllare la tua connessione Internet" + }, + "main_screen": { + "Version": "Versione", + "Device": "Dispositivo", + "select a new one": "selezionane uno nuovo", + "Flash": "Flash il firmware", + "Wipe": "Spegnere il dispositivo", + "Settings": "Configurazioni", + "About": "Informazioni", + "Fetching data from": "Recupero dati da" + }, + "select_version_screen": { + "Old versions": "Vecchie versioni", + "Back": "Indietro" + }, + "select_old_version_screen": { + "Back": "Indietro" + }, + "warning_beta_screen": { + "WARNING": "AVVERTIMENTO", + "This is our test repository": "Questo è il nostro repository di testare", + "These are unsigned binaries for the latest and most experimental features": "Questi sono file binari non firmati per le funzionalità più recenti e sperimentali", + "and it's just for trying new things and providing feedback.": "ed è solo per provare cose nuove e fornire feedback.", + "Proceed": "Procedere", + "Back": "Indietro" + }, + "download_beta_screen": { + "Connecting": "Connessione a", + "Downloading": "Download dell'", + "to": "nella", + "of": "di", + "downloaded": "scaricato" + }, + "warning_already_downloaded_screen": { + "Assets already downloaded": "Risorse già scaricate", + "Do you want to proceed with the same file or do you want to download it again?": "Vuoi procedere con l'ultimo file o come scaricare il nuovo file?", + "Download again": "Scarica di nuovo", + "Proceed with current file": "Procedi con il file corrente" + }, + "download_stable_zip_screen": { + "Connecting": "Connessione a", + "Downloading": "Download dell'", + "to": "nella", + "of": "di", + "downloaded": "scaricato" + }, + "download_stable_zip_sha256_screen": { + "Connecting": "Connessione a", + "Downloading": "Download dell'", + "to": "nella", + "of": "di", + "downloaded": "scaricato" + }, + "download_stable_zip_sig_screen": { + "Connecting": "Connessione a", + "Downloading": "Download dell'", + "to": "nella", + "of": "di", + "downloaded": "scaricato" + }, + "download_selfcustody_pem_screen": { + "Connecting": "Connessione a", + "Downloading": "Download dell'", + "to": "nella", + "of": "di", + "downloaded": "scaricato" + }, + "verify_stable_zip_screen": { + "Verifying integrity and authenticity": "Verifica dell'integrità e dell'autenticità", + "Integrity verification": "Verifica dell'integrità", + "computed hash from": "hash calcolato da", + "provided hash from": "hash fornito da", + "SUCCESS": "SUCCESSO", + "FAILED": "FALLITO", + "Authenticity verification": "Verifica dell'autenticità", + "GOOD": "BUONA", + "BAD": "CATTIVA", + "SIGNATURE": "FIRMA", + "If you have openssl installed on your system": "Se hai openssl installato sul tuo sistema", + "you can check manually with the following command": "è possibile controllare manualmente con il seguente comando", + "Proceed": "Procedere", + "Back": "Indietro" + }, + "unzip_stable_screen": { + "Flash with": "Flash con", + "Air-gapped update with": "Aggiornamento isolato con (a breve)", + "Unziping": "Decompressione", + "Unziped": "Decompresso" + }, + "flash_screen": { + "DONE": "FATTO", + "Back": "Indietro", + "Quit": "Dimettersi", + "Flashing": "Facendo flash do", + "at": "a" + }, + "wipe_screen": { + "PLEASE DO NOT UNPLUG YOUR DEVICE": "SI PREGA DI NON SCOLLEGARE IL DISPOSITIVO", + "DONE": "FATTO", + "Back": "Indietro", + "Quit": "Dimettersi" + }, + "warning_wipe_screen": { + "You are about to initiate a FULL WIPE of this device": "Stai per avviare una CANCELLAZIONE COMPLETA di questo dispositivo", + "This operation will": "Questa operazione", + "Permanently erase all saved data": "Cancella definitivamente tutti i dati salvati", + "Remove the existing firmware": "Rimuovere il firmware esistente", + "Render the device non-functional until new firmware is re-flashed": "Renderi il dispositivo non funzionante fino a quando non viene eseguito nuovamente il flashing del nuovo firmware", + "Proceed": "Procedere", + "Back": "Indietro" + }, + "about_screen": { + "follow us on X": "seguici su X", + "Back": "Ritirati" + } +} diff --git a/src/i18n/ko_KR.UTF-8.json b/src/i18n/ko_KR.UTF-8.json new file mode 100644 index 00000000..f545eb33 --- /dev/null +++ b/src/i18n/ko_KR.UTF-8.json @@ -0,0 +1,135 @@ +{ + "check_permissions_screen": { + "Setup": "설치", + "for": "때문에", + "Checking": "확인", + "permissions for": "에 대한 권한", + "WARNING": "경고", + "This is the first run of KruxInstaller in": "이것은 KruxInstaller의 첫 번째 실행입니다.", + "and it appears that you do not have privileged access to make flash procedures": "그리고 플래시 절차를 만들 수있는 권한이없는 것 같습니다.", + "To proceed, click in the Allow button and a prompt will ask for your password": "To proceed, click in the Allow button and a prompt will ask for your password", + "to execute the following command": "다음 명령을 실행합니다", + "You may need to logout (or even reboot)": "로그아웃(또는 재부팅)이 필요할 수 있습니다.", + "and back in for the new group to take effect": "그리고 새 그룹이 적용되도록 다시 참여합니다.", + "Do not worry, this message won't appear again": "이 메시지는 다시 나타나지 않으므로 걱정하지 마십시오", + "Allow": "Allow", + "Deny": "Deny" + }, + "check_internet_connection_screen": { + "Checking your internet connection": "인터넷 연결 확인" + }, + "main_screen": { + "Version": "버전", + "Device": "장치", + "select a new one": "새 것을 선택하십시오", + "Flash": "플래시 펌웨어", + "Wipe": "장치 지우기", + "Settings": "설정", + "About": "요약", + "Fetching data from": "데이터 가져오기" + }, + "select_version_screen": { + "Old versions": "이전 버전", + "Back": "뒤로" + }, + "select_old_version_screen": { + "Back": "뒤로" + }, + "warning_beta_screen": { + "WARNING": "경고", + "This is our test repository": "이것은 우리의 테스트 저장소입니다", + "These are unsigned binaries for the latest and most experimental features": "이는 최신 및 가장 실험적인 기능에 대한 서명되지 않은 이진 파일입니다", + "and it's just for trying new things and providing feedback.": "그리고 그것은 단지 새로운 것을 \n시도하고 피드백을 제공하기 위한 것입니다.", + "Proceed": "진행", + "Back": "뒤로" + }, + "download_beta_screen": { + "Connecting": "연결", + "Downloading": "다운로드", + "to": "l에", + "of": "의", + "downloaded": "다운로드한 파일" + }, + "warning_already_downloaded_screen": { + "Assets already downloaded": "이미 다운로드한 자산", + "Do you want to proceed with the same file or do you want to download it again?": "동일한 파일로 계속 진행하시겠습니까, 아니면 다시 다운로드하시겠습니까?", + "Download again": "다시 다운로드", + "Proceed with current file": "현재 파일로 계속" + }, + "download_stable_zip_screen": { + "Connecting": "연결", + "Downloading": "다운로드", + "to": "l에", + "of": "의", + "downloaded": "다운로드한 파일" + }, + "download_stable_zip_sha256_screen": { + "Connecting": "연결", + "Downloading": "다운로드", + "to": "l에", + "of": "의", + "downloaded": "다운로드한 파일" + }, + "download_stable_zip_sig_screen": { + "Connecting": "연결", + "Downloading": "다운로드", + "to": "l에", + "of": "의", + "downloaded": "다운로드한 파일" + }, + "download_selfcustody_pem_screen": { + "Connecting": "연결", + "Downloading": "다운로드", + "to": "l에", + "of": "의", + "downloaded": "다운로드한 파일" + }, + "verify_stable_zip_screen": { + "Verifying integrity and authenticity": "무결성 및 신뢰성 확인", + "Integrity verification": "무결성 검증", + "computed hash from": "계산된 해시", + "provided hash from": "다음에서 제공된 해시", + "SUCCESS": "성공", + "FAILED": "실패", + "Authenticity verification": "진위 여부 확인", + "GOOD": "좋다", + "BAD": "나쁘다", + "SIGNATURE": "서명", + "If you have openssl installed on your system": "시스템에 openssl이 설치되어 있는 경우", + "you can check manually with the following command": "다음 명령으로 확인할 수 있습니다", + "Proceed": "진행", + "Back": "뒤로" + }, + "unzip_stable_screen": { + "Flash with": "플래시 사용", + "Air-gapped update with": "격리 업데이트(곧 제공 예정)", + "Unziping": "추출", + "Unziped": "추출" + }, + "flash_screen": { + "DONE": "수행", + "Back": "뒤로", + "Quit": "사임하다", + "Flashing": "플래시 수행 중", + "at": "에" + }, + "wipe_screen": { + "PLEASE DO NOT UNPLUG YOUR DEVICE": "장치의 플러그를 뽑지 마십시오.", + "DONE": "수행", + "Back": "뒤로", + "Quit": "사임하다" + }, + "warning_wipe_screen": { + "You are about to initiate a FULL WIPE of this device": "이 장치의 전체 지우기를 시작하려고 합니다.", + "This operation will": "이 작업은 다음을 수행합니다.", + "Permanently erase all saved data": "저장된 모든 데이터를 영구적으로 지우기", + "Remove the existing firmware": "기존 펌웨어를 제거합니다", + "Render the device non-functional until new firmware is re-flashed": "새 펌웨어가 다시 플래시될 때까지 장치를 작동하지 않게 만듭니다.", + "Proceed": "진행", + "Back": "뒤로" + }, + "about_screen": { + "follow us on X": "X에서 우리를 따르십시오", + "Back": "뒤로" + } +} diff --git a/src/i18n/nl_NL.UTF-8.json b/src/i18n/nl_NL.UTF-8.json new file mode 100644 index 00000000..e251bb3a --- /dev/null +++ b/src/i18n/nl_NL.UTF-8.json @@ -0,0 +1,133 @@ +{ + "check_permissions_screen": { + "Setup": "Installatie", + "for": "voor", + "Checking": "Controleren", + "permissions for": "Permissie voor", + "WARNING": "WAARSCHUWING", + "This is the first run of KruxInstaller in": "De eerste keer gebruikmaken van KruxInstaller in", + "and it appears that you do not have privileged access to make flash procedures": "en het blijkt dat er geen voldoende rechten zijn om te flashen", + "To proceed, click in the Allow button and a prompt will ask for your password": "Om verder te gaan, klik op de knop 'Toestaan' en vul je wachtwoord in", + "to execute the following command": "om de volgende opdracht uit te voeren", + "You may need to logout (or even reboot)": "Mogelijk moet je je afmelden (of zelfs opnieuw opstarten)", + "and back in for the new group to take effect": "om de nieuw aangemaakte groep te activeren", + "Do not worry, this message won't appear again": "Maak je geen zorgen, dit bericht verschijnt niet meer", + "Allow": "Toestaan", + "Deny": "Annuleren" + }, + "check_internet_connection_screen": { + "Checking your internet connection": "Je internetverbinding controleren" + }, + "main_screen": { + "Version": "Versie", + "Device": "Apparaat", + "select a new one": "selecteer een nieuwe", + "Flash": "Flash de firmware", + "Wipe": "Apparaat wissen", + "Settings": "Instellingen", + "About": "Over", + "Fetching data from": "Gegevens ophalen van" + }, + "select_version_screen": { + "Old versions": "Oude versies", + "Back": "Terug" + }, + "select_old_version_screen": { + "Back": "Terug" + }, + "warning_beta_screen": { + "WARNING": "WAARSCHUWING", + "This is our test repository": "Dit is onze test omgeving", + "These are unsigned binaries for the latest and most experimental features": "Dit zijn niet ondergetekende binaire bestanden voor\nde nieuwste en meest experimentele functies", + "and it's just for trying new things and providing feedback.": "om nieuwe dingen te proberen en feedback te geven.", + "Proceed": "Doorgaan", + "Back": "Terug" + }, + "download_beta_screen": { + "Connecting": "Verbinden", + "Downloading": "Downloaden van", + "to": "naar", + "of": "van", + "downloaded": "gedownload" + }, + "warning_already_downloaded_screen": { + "Assets already downloaded": "Eerder gedownloade items", + "Do you want to proceed with the same file or do you want to download it again?": "Wil je doorgaan met hetzelfde bestand of wil je het opnieuw downloaden?", + "Download again": "Opnieuw downloaden", + "Proceed with current file": "Ga verder met het huidige bestand" + }, + "download_stable_zip_screen": { + "Connecting": "Verbinden", + "Downloading": "Downloaden van", + "to": "naar", + "of": "van", + "downloaded": "gedownload" + }, + "download_stable_zip_sha256_screen": { + "Connecting": "Verbinden", + "Downloading": "Downloaden van", + "to": "naar", + "of": "van", + "downloaded": "gedownload" + }, + "download_stable_zip_sig_screen": { + "Connecting": "Verbinden", + "Downloading": "Downloaden van", + "to": "naar", + "of": "van", + "downloaded": "gedownload" + }, + "download_selfcustody_pem_screen": { + "Connecting": "Verbinden", + "Downloading": "Downloaden van", + "to": "naar", + "of": "van", + "downloaded": "gedownload" + }, + "verify_stable_zip_screen": { + "Verifying integrity and authenticity": "Integriteit en authenticiteit verifiëren", + "Integrity verification": "Verificatie van de integriteit", + "computed hash from": "berekende hash van", + "provided hash from": "verstrekte hash van", + "SUCCESS": "SUCCES", + "FAILED": "MISLUKT", + "Authenticity verification": "Verificatie van echtheid", + "GOOD": "GOED", + "BAD": "SLECHT", + "SIGNATURE": "HANDTEKENING", + "If you have openssl installed on your system": "Als je openssl op je systeem hebt geïnstalleerd", + "you can check manually with the following command": "kan je dit controleren met de volgende commando", + "Proceed": "Doorgaan", + "Back": "Terug" + }, + "unzip_stable_screen": { + "Flash with": "Flash met", + "Air-gapped update with": "Update zonder computer met (binnenkort)", + "Unziping": "Uitpakken", + "Unziped": "Uitgepakt" + }, + "flash_screen": { + "DONE": "KLAAR", + "Back": "Terug", + "Quit": "Stoppen" + }, + "wipe_screen": { + "PLEASE DO NOT UNPLUG YOUR DEVICE": "KOPPEL HET APPARAAT NIET LOS", + "DONE": "KLAAR", + "Back": "Terug", + "Quit": "Stoppen" + }, + "warning_wipe_screen": { + "You are about to initiate a FULL WIPE of this device": "Je staat op het punt om te starten met het VOLLEDIGE WISSEN van dit apparaat", + "This operation will": "Deze operatie is zal", + "Permanently erase all saved data": "Alle opgeslagen gegevens permanent wissen", + "Remove the existing firmware": "Verwijderen van de bestaande firmware", + "Render the device non-functional until new firmware is re-flashed": "Maakt het apparaat niet meer functioneel totdat de nieuwste firmware opnieuw geflasht is", + "Proceed": "Doorgaan", + "Back": "Terug" + }, + "about_screen": { + "follow us on X": "volg ons op X", + "Back": "Terug" + } +} diff --git a/src/i18n/pt_BR.UTF-8.json b/src/i18n/pt_BR.UTF-8.json new file mode 100644 index 00000000..d5184211 --- /dev/null +++ b/src/i18n/pt_BR.UTF-8.json @@ -0,0 +1,135 @@ +{ + "check_permissions_screen": { + "Setup": "Configurando", + "for": "para", + "Checking": "Checando", + "permissions for": "permissiões para", + "WARNING": "AVISO", + "This is the first run of KruxInstaller in": "Esta é a primeira execução do KruxInstaller no", + "and it appears that you do not have privileged access to make flash procedures": "e parece que você não tem acesso privilegiado para realizar procedimentos de flash", + "To proceed, click in the Allow button and a prompt will ask for your password": "Para continuar, clique no botão Permitir e um prompt solicitará sua senha", + "to execute the following command": "para executar o seguinte comando", + "You may need to logout (or even reboot)": "Talvez você precise deslogar (ou até mesmo reiniciar)", + "and back in for the new group to take effect": "e re-logar para que o novo grupo tenha efeito", + "Do not worry, this message won't appear again": "Não se preocupe, essa mensagem não irá aparecer novamente", + "Allow": "Permitir", + "Deny": "Negar" + }, + "check_internet_connection_screen": { + "Checking your internet connection": "Checando sua conexão com a internet" + }, + "main_screen": { + "Version": "Versão", + "Device": "Dispositivo", + "select a new one": "selecione um novo", + "Flash": "Flash o firmware", + "Wipe": "Limpar dispositivo", + "Settings": "Configurações", + "About": "Sobre", + "Fetching data from": "Buscando dados de" + }, + "select_version_screen": { + "Old versions": "Versões antigas", + "Back": "Voltar" + }, + "select_old_version_screen": { + "Back": "Voltar" + }, + "warning_beta_screen": { + "WARNING": "ADVERTÊNCIA", + "This is our test repository": "Este é nosso repositório de testes", + "These are unsigned binaries for the latest and most experimental features": "Estes são binários não assinados dos recursos mais experimentais", + "and it's just for trying new things and providing feedback.": "e serve apenas para experimentar coisas novas e dar opiniões.", + "Proceed": "Proceder", + "Back": "Voltar" + }, + "download_beta_screen": { + "Connecting": "Conectando", + "Downloading": "Baixando", + "to": "para", + "of": "de", + "downloaded": "baixado" + }, + "warning_already_downloaded_screen": { + "Assets already downloaded": "Arquivos já baixados", + "Do you want to proceed with the same file or do you want to download it again?": "Você quer proceder com o mesmo arquivo ou quer fazer o download dele novamente?", + "Download again": "Baixe novamente", + "Proceed with current file": "Proceda com o arquivo atual" + }, + "download_stable_zip_screen": { + "Connecting": "Conectando", + "Downloading": "Baixando", + "to": "para", + "of": "de", + "downloaded": "baixado" + }, + "download_stable_zip_sha256_screen": { + "Connecting": "Conectando", + "Downloading": "Baixando", + "to": "para", + "of": "de", + "downloaded": "baixado" + }, + "download_stable_zip_sig_screen": { + "Connecting": "Conectando", + "Downloading": "Baixando", + "to": "para", + "of": "de", + "downloaded": "baixado" + }, + "download_selfcustody_pem_screen": { + "Connecting": "Conectando", + "Downloading": "Baixando", + "to": "para", + "of": "de", + "downloaded": "baixado" + }, + "verify_stable_zip_screen": { + "Verifying integrity and authenticity": "Verificando integridade e autenticidade", + "Integrity verification": "Verificação de integridade", + "computed hash from": "hash computado de", + "provided hash from": "hash fornecido de", + "SUCCESS": "SUCESSO", + "FAILED": "FALHOU", + "Authenticity verification": "Verificação de autenticidade", + "GOOD": "BOA", + "BAD": "MÁ", + "SIGNATURE": "ASSINATURA", + "If you have openssl installed on your system": "Se você tiver o openssl instalado no seu sistema,", + "you can check manually with the following command": "você pode checar manualmente a com o seguinte commando", + "Proceed": "Proceder", + "Back": "Voltar" + }, + "unzip_stable_screen": { + "Flash with": "Flash com", + "Air-gapped update with": "Atualização isolada com (em breve)", + "Unziping": "Descomprimindo", + "Unziped": "Descomprimido" + }, + "flash_screen": { + "DONE": "FEITO ", + "Back": "Voltar", + "Quit": "Sair", + "Flashing": "Fazendo flash do", + "at": "a" + }, + "wipe_screen": { + "PLEASE DO NOT UNPLUG YOUR DEVICE": "POR FAVOR NÃO DESPLUGUE SEU DISPOSITIVO", + "DONE": "FEITO", + "Back": "Voltar", + "Quit": "Sair" + }, + "warning_wipe_screen": { + "You are about to initiate a FULL WIPE of this device": "Você está prestes a iniciar uma limpeza completa deste dispositivo", + "This operation will": "Esta operação irá", + "Permanently erase all saved data": "Apagar permanentemente todos os dados salvos", + "Remove the existing firmware": "Remover o firmware existente", + "Render the device non-functional until new firmware is re-flashed": "Tornar o dispositivo não funcional até que o novo firmware seja atualizado novamente", + "Proceed": "Proceder", + "Back": "Voltar" + }, + "about_screen": { + "follow us on X": "siga-nos no X", + "Back": "Voltar" + } +} diff --git a/src/i18n/ru_RU.UTF-8.json b/src/i18n/ru_RU.UTF-8.json new file mode 100644 index 00000000..5614d922 --- /dev/null +++ b/src/i18n/ru_RU.UTF-8.json @@ -0,0 +1,135 @@ +{ + "check_permissions_screen": { + "Setup": "Настройка", + "for": "для", + "Checking": "Проверка", + "permissions for": "Разрешения для", + "WARNING": "ПРЕДУПРЕЖДЕНИЕ", + "This is the first run of KruxInstaller in": "Это первый запуск KruxInstaller в", + "and it appears that you do not have privileged access to make flash procedures": "и оказывается, что у вас нет привилегированного доступа для выполнения процедур прошивки", + "To proceed, click in the Allow button and a prompt will ask for your password": "Чтобы продолжить, нажмите кнопку «Разрешить», и появится запрос на ввод пароля", + "to execute the following command": ", чтобы выполнить следующую команду", + "You may need to logout (or even reboot)": "Возможно, потребуется перезагрузка (или даже перезагрузка)", + "and back in for the new group to take effect": "и возвращайтесь снова, чтобы новая группа вступила в силу", + "Do not worry, this message won't appear again": "Не волнуйтесь, это сообщение больше не появится", + "Allow": "Разрешать", + "Deny": "Отрицать" + }, + "check_internet_connection_screen": { + "Checking your internet connection": "Проверка подключения к Интернету" + }, + "main_screen": { + "Version": "Версия", + "Device": "Устройство", + "select a new one": "выберите новый", + "Flash": "Прошивка прошивки", + "Wipe": "Стереть устройство", + "Settings": "Настройки", + "About": "Об установщике", + "Fetching data from": "Получение данных из" + }, + "select_version_screen": { + "Old versions": "Старые версии", + "Back": "Вернуться" + }, + "select_old_version_screen": { + "Back": "Вернуться" + }, + "warning_beta_screen": { + "WARNING": "ПРЕДУПРЕЖДЕНИЕ", + "This is our test repository": "Это наш тестовый репозиторий", + "These are unsigned binaries for the latest and most experimental features": "Это неподписанные двоичные\nфайлы для новейших и наиболее экспериментальных функций", + "and it's just for trying new things and providing feedback.": "и это просто для того, чтобы попробовать\nчто-то новое и оставить отзыв", + "Proceed": "Продолжать", + "Back": "Вернуться" + }, + "download_beta_screen": { + "Connecting": "Соединительный", + "Downloading": "Скачивание", + "to": "в", + "of": "из", + "downloaded": "Загрузить" + }, + "warning_already_downloaded_screen": { + "Assets already downloaded": "Ресурсы уже загружены", + "Do you want to proceed with the same file or do you want to download it again?": "Хотите продолжить с последним файлом или как загрузить новый файл?", + "Download again": "Скачать еще раз", + "Proceed with current file": "Продолжить работу с текущим файлом" + }, + "download_stable_zip_screen": { + "Connecting": "Соединительный", + "Downloading": "Загрузки", + "to": "в", + "of": "из", + "downloaded": "Загрузить" + }, + "download_stable_zip_sha256_screen": { + "Connecting": "Соединительный", + "Downloading": "Загрузки", + "to": "в", + "of": "из", + "downloaded": "Загрузить" + }, + "download_stable_zip_sig_screen": { + "Connecting": "Соединительный", + "Downloading": "Загрузки", + "to": "в", + "of": "из", + "downloaded": "Загрузить" + }, + "download_selfcustody_pem_screen": { + "Connecting": "Соединительный", + "Downloading": "Загрузки", + "to": "в", + "of": "из", + "downloaded": "Загрузить" + }, + "verify_stable_zip_screen": { + "Verifying integrity and authenticity": "Проверка целостности и подлинности", + "Integrity verification": "Проверка целостности", + "computed hash from": "вычисленный хеш из", + "provided hash from": "предоставленный хеш из", + "SUCCESS": "УСПЕХ", + "FAILED": "НЕУДАВШИЙСЯ", + "Authenticity verification": "Проверка подлинности", + "GOOD": "ХОРОШАЯ", + "BAD": "НЕПРАВИЛЬНАЯ", + "SIGNATURE": "ПОДПИСЬ", + "If you have openssl installed on your system": "Если в вашей системе установлен openssl", + "you can check manually with the following command": "Вы можете проверить вручную с помощью следующей команды", + "Proceed": "Продолжать", + "Back": "Вернись" + }, + "unzip_stable_screen": { + "Flash with": "Вспышка с", + "Air-gapped update with": "Изолированное обновление с (скоро будет)", + "Unziping": "Декомпрессия", + "Unziped": "Неупакованный" + }, + "flash_screen": { + "DONE": "ДОГОВОРИЛИСЬ", + "Back": "Назад", + "Quit": "Покидать", + "Flashing": "Перепрошивка на", + "at": "на" + }, + "wipe_screen": { + "PLEASE DO NOT UNPLUG YOUR DEVICE": "ПОЖАЛУЙСТА, НЕ ОТКЛЮЧАЙТЕ УСТРОЙСТВО ОТ СЕТИ", + "DONE": "ДОГОВОРИЛИСЬ", + "Back": "Назад", + "Quit": "Покидать" + }, + "warning_wipe_screen": { + "You are about to initiate a FULL WIPE of this device": "Вы собираетесь инициировать ПОЛНУЮ ОЧИСТКУ этого устройства", + "This operation will": "Эта операция позволит", + "Permanently erase all saved data": "Безвозвратное стирание всех сохраненных данных", + "Remove the existing firmware": "Удалите существующую прошивку", + "Render the device non-functional until new firmware is re-flashed": "Сделайте устройство неработоспособным до тех пор, пока не будет перепрошита новая прошивка", + "Proceed": "Продолжать", + "Back": "Вернись" + }, + "about_screen": { + "follow us on X": "следите за нами на X", + "Back": "Назад" + } +} diff --git a/src/i18n/zh_CN.UTF-8.json b/src/i18n/zh_CN.UTF-8.json new file mode 100644 index 00000000..f349660e --- /dev/null +++ b/src/i18n/zh_CN.UTF-8.json @@ -0,0 +1,135 @@ +{ + "check_permissions_screen": { + "Setup": "设置", + "for": "为", + "Checking": "检查", + "permissions for": "的权限", + "WARNING": "警告", + "This is the first run of KruxInstaller in": "这是 KruxInstaller 的首次运行。", + "and it appears that you do not have privileged access to make flash procedures": "而且您似乎没有特权访问权限来执行闪存过程", + "To proceed, click in the Allow button and a prompt will ask for your password": "要继续,请单击\"允许\"按钮,系统将提示您输入密码", + "to execute the following command": "执行以下命令", + "You may need to logout (or even reboot)": "您可能需要注销(甚至重新启动)", + "and back in for the new group to take effect": "并返回以使新组生效", + "Do not worry, this message won't appear again": "不用担心,此消息不会再次出现", + "Allow": "允许", + "Deny": "否认" + }, + "check_internet_connection_screen": { + "Checking your internet connection": "检查您的互联网连接" + }, + "main_screen": { + "Version": "版本", + "Device": "装置", + "select a new one": "选择一个新的", + "Flash": "闪存固件", + "Wipe": "擦除设备", + "Settings": "设置", + "About": "大约", + "Fetching data from": "从中获取数据" + }, + "select_version_screen": { + "Old versions": "旧版本", + "Back": "返回" + }, + "select_old_version_screen": { + "Back": "返回" + }, + "warning_beta_screen": { + "WARNING": "警告", + "This is our test repository": "这是我们的测试仓库", + "These are unsigned binaries for the latest and most experimental features": "这些是最新和最具实验性功能的无符号二进制文件", + "and it's just for trying new things and providing feedback.": "它只是为了尝试新事物和提供反馈。", + "Proceed": "进行", + "Back": "返回" + }, + "download_beta_screen": { + "Connecting": "连接", + "Downloading": "下载", + "to": "自", + "of": "之", + "downloaded": "下载" + }, + "warning_already_downloaded_screen": { + "Assets already downloaded": "已下载的资产", + "Do you want to proceed with the same file or do you want to download it again?": "是要继续处理同一文件,还是要再次下载?", + "Download again": "再次下载", + "Proceed with current file": "继续处理当前文件" + }, + "download_stable_zip_screen": { + "Connecting": "连接", + "Downloading": "下载", + "to": "自", + "of": "之", + "downloaded": "下载" + }, + "download_stable_zip_sha256_screen": { + "Connecting": "连接", + "Downloading": "下载", + "to": "自", + "of": "之", + "downloaded": "下载" + }, + "download_stable_zip_sig_screen": { + "Connecting": "连接", + "Downloading": "下载", + "to": "自", + "of": "之", + "downloaded": "下载" + }, + "download_selfcustody_pem_screen": { + "Connecting": "连接", + "Downloading": "下载", + "to": "自", + "of": "之", + "downloaded": "下载" + }, + "verify_stable_zip_screen": { + "Verifying integrity and authenticity": "验证完整性和真实性", + "Integrity verification": "完整性验证", + "computed hash from": "计算出的哈希值", + "provided hash from": "提供的哈希值来自", + "SUCCESS": "成功", + "FAILED": "失败", + "Authenticity verification": "真实性验证", + "GOOD": "好", + "BAD": "坏", + "SIGNATURE": "签名", + "If you have openssl installed on your system": "如果您的系统上安装了 openssl", + "you can check manually with the following command": "您可以使用以下命令手动检查", + "Proceed": "进行", + "Back": "返回" + }, + "unzip_stable_screen": { + "Flash with": "闪光灯", + "Air-gapped update with": "隔离更新 (即将推出)", + "Unziping": "解压缩", + "Unziped": "解压" + }, + "flash_screen": { + "DONE": "做", + "Back": "返回", + "Quit": "退出", + "Flashing": "闪烁", + "at": "在" + }, + "wipe_screen": { + "PLEASE DO NOT UNPLUG YOUR DEVICE": "请不要拔下您的设备", + "DONE": "做", + "Back": "返回", + "Quit": "退出" + }, + "warning_wipe_screen": { + "You are about to initiate a FULL WIPE of this device": "您将要启动此设备的完全擦除", + "This operation will": "此操作将", + "Permanently erase all saved data": "永久擦除所有已保存的数据", + "Remove the existing firmware": "删除现有固件", + "Render the device non-functional until new firmware is re-flashed": "在重新刷新新固件之前,使设备无法正常工作", + "Proceed": "进行", + "Back": "返回" + }, + "about_screen": { + "follow us on X": "在X上关注我们", + "Back": "返回" + } +} diff --git a/src/main.ts b/src/main.ts deleted file mode 100644 index 66e3447e..00000000 --- a/src/main.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createApp } from 'vue' -import { createVuetify } from 'vuetify' -import { aliases, mdi } from 'vuetify/iconsets/mdi-svg' -import 'vuetify/styles' -import "./style.css" -import App from './App.vue' - -// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides -const vuetify = createVuetify({ - icons: { - defaultSet: 'mdi', - aliases, - sets: { - mdi - } - } -}) - -createApp(App) - .use(vuetify) - .mount('#app') - .$nextTick(() => { - postMessage({ payload: 'removeLoading' }, '*') - }) diff --git a/src/pages/CheckVerifyOfficialRelease.vue b/src/pages/CheckVerifyOfficialRelease.vue deleted file mode 100644 index 010bf74f..00000000 --- a/src/pages/CheckVerifyOfficialRelease.vue +++ /dev/null @@ -1,81 +0,0 @@ - - - \ No newline at end of file diff --git a/src/pages/ConsoleLoad.vue b/src/pages/ConsoleLoad.vue deleted file mode 100644 index eaad26cc..00000000 --- a/src/pages/ConsoleLoad.vue +++ /dev/null @@ -1,59 +0,0 @@ - - - \ No newline at end of file diff --git a/src/pages/DownloadOfficialReleasePem.vue b/src/pages/DownloadOfficialReleasePem.vue deleted file mode 100644 index 36772f1c..00000000 --- a/src/pages/DownloadOfficialReleasePem.vue +++ /dev/null @@ -1,55 +0,0 @@ -. - - \ No newline at end of file diff --git a/src/pages/DownloadOfficialReleaseSha256.vue b/src/pages/DownloadOfficialReleaseSha256.vue deleted file mode 100644 index fad9156a..00000000 --- a/src/pages/DownloadOfficialReleaseSha256.vue +++ /dev/null @@ -1,55 +0,0 @@ - - - \ No newline at end of file diff --git a/src/pages/DownloadOfficialReleaseSig.vue b/src/pages/DownloadOfficialReleaseSig.vue deleted file mode 100644 index 87eaf0e7..00000000 --- a/src/pages/DownloadOfficialReleaseSig.vue +++ /dev/null @@ -1,55 +0,0 @@ - - - \ No newline at end of file diff --git a/src/pages/DownloadOfficialReleaseZip.vue b/src/pages/DownloadOfficialReleaseZip.vue deleted file mode 100644 index 4178a375..00000000 --- a/src/pages/DownloadOfficialReleaseZip.vue +++ /dev/null @@ -1,55 +0,0 @@ - - - \ No newline at end of file diff --git a/src/pages/DownloadTestFirmware.vue b/src/pages/DownloadTestFirmware.vue deleted file mode 100644 index 3b3c43ee..00000000 --- a/src/pages/DownloadTestFirmware.vue +++ /dev/null @@ -1,55 +0,0 @@ - - - \ No newline at end of file diff --git a/src/pages/DownloadTestKboot.vue b/src/pages/DownloadTestKboot.vue deleted file mode 100644 index f1a6f098..00000000 --- a/src/pages/DownloadTestKboot.vue +++ /dev/null @@ -1,55 +0,0 @@ - - - \ No newline at end of file diff --git a/src/pages/DownloadTestKtool.vue b/src/pages/DownloadTestKtool.vue deleted file mode 100644 index b49d7776..00000000 --- a/src/pages/DownloadTestKtool.vue +++ /dev/null @@ -1,55 +0,0 @@ - - - \ No newline at end of file diff --git a/src/pages/ErrorMsg.vue b/src/pages/ErrorMsg.vue deleted file mode 100644 index fbd852a6..00000000 --- a/src/pages/ErrorMsg.vue +++ /dev/null @@ -1,91 +0,0 @@ - - - diff --git a/src/pages/FlashToDevice.vue b/src/pages/FlashToDevice.vue deleted file mode 100644 index 419ae137..00000000 --- a/src/pages/FlashToDevice.vue +++ /dev/null @@ -1,89 +0,0 @@ - - - - - diff --git a/src/pages/GithubChecker.vue b/src/pages/GithubChecker.vue deleted file mode 100644 index 9e18f1bc..00000000 --- a/src/pages/GithubChecker.vue +++ /dev/null @@ -1,86 +0,0 @@ - - - \ No newline at end of file diff --git a/src/pages/KruxInstallerLogo.vue b/src/pages/KruxInstallerLogo.vue deleted file mode 100644 index 044a9097..00000000 --- a/src/pages/KruxInstallerLogo.vue +++ /dev/null @@ -1,61 +0,0 @@ - - - diff --git a/src/pages/Main.vue b/src/pages/Main.vue deleted file mode 100644 index 240435af..00000000 --- a/src/pages/Main.vue +++ /dev/null @@ -1,190 +0,0 @@ - - - diff --git a/src/pages/SelectDevice.vue b/src/pages/SelectDevice.vue deleted file mode 100644 index b27e3725..00000000 --- a/src/pages/SelectDevice.vue +++ /dev/null @@ -1,68 +0,0 @@ - - - diff --git a/src/pages/SelectVersion.vue b/src/pages/SelectVersion.vue deleted file mode 100644 index 4a5040d8..00000000 --- a/src/pages/SelectVersion.vue +++ /dev/null @@ -1,62 +0,0 @@ - - - diff --git a/src/pages/VerifiedOfficialRelease.vue b/src/pages/VerifiedOfficialRelease.vue deleted file mode 100644 index bbb6f88a..00000000 --- a/src/pages/VerifiedOfficialRelease.vue +++ /dev/null @@ -1,130 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/pages/WarningDownload.vue b/src/pages/WarningDownload.vue deleted file mode 100644 index a5ecfca7..00000000 --- a/src/pages/WarningDownload.vue +++ /dev/null @@ -1,194 +0,0 @@ - - - diff --git a/src/pages/WipeDevice.vue b/src/pages/WipeDevice.vue deleted file mode 100644 index d492d03b..00000000 --- a/src/pages/WipeDevice.vue +++ /dev/null @@ -1,89 +0,0 @@ - - - - - diff --git a/src/style.css b/src/style.css deleted file mode 100644 index 99827c19..00000000 --- a/src/style.css +++ /dev/null @@ -1,90 +0,0 @@ -:root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #000000; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-text-size-adjust: 100%; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -code { - background-color: #1a1a1a; - padding: 2px 4px; - margin: 0 4px; - border-radius: 4px; -} - -.card { - padding: 2em; -} - -#app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } - code { - background-color: #f9f9f9; - } -} diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/utils/constants/__init__.py b/src/utils/constants/__init__.py new file mode 100644 index 00000000..e58632d9 --- /dev/null +++ b/src/utils/constants/__init__.py @@ -0,0 +1,102 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +constants.py + +Some constants to be used accros application +""" + +from typing import Any +import sys +import os + + +ROOT_DIRNAME = os.path.abspath(os.path.dirname(__file__)) + +VALID_DEVICES_VERSIONS = { + "v24.07.0": ["m5stickv", "amigo", "dock", "bit", "yahboom", "cube"], + "v24.03.0": ["m5stickv", "amigo", "dock", "bit", "yahboom"], + "v23.09.1": ["m5stickv", "amigo", "dock", "bit"], + "v23.09.0": ["m5stickv", "amigo", "dock", "bit"], + "v22.08.2": ["m5stickv", "amigo", "dock", "bit"], + "v22.08.1": ["m5stickv", "amigo", "dock", "bit"], + "v22.08.0": ["m5stickv", "amigo", "dock", "bit"], + "v22.03.0": ["m5stickv"], + "odudex/krux_binaries": [ + "m5stickv", + "amigo", + "dock", + "bit", + "yahboom", + "dock", + "cube", + "wonder_mv", + ], +} + + +def _open_pyproject() -> dict[str, Any]: + """ + Open root pyprojet.toml file to get some constant datas + like name, version and description + """ + if sys.version_info.minor <= 10: + # pylint: disable=import-outside-toplevel + from tomli import loads as load_toml + if sys.version_info.minor > 10: + # pylint: disable=import-outside-toplevel,import-error + from tomllib import loads as load_toml + + try: + pyproject_filename = os.path.abspath( + os.path.join(ROOT_DIRNAME, "..", "..", "..", "pyproject.toml") + ) + with open(pyproject_filename, "r", encoding="utf8") as pyproject_file: + data = pyproject_file.read() + # pylint: disable=possibly-used-before-assignment + return load_toml(data) + + except FileNotFoundError as exc: + raise FileNotFoundError(f"{pyproject_filename} isnt found") from exc + except ValueError as exc: + raise ValueError(f"{pyproject_filename} is not valid toml file") from exc + + +def get_name() -> str: + """ + Get project name defined in pyproject.toml + """ + return _open_pyproject()["tool"]["poetry"]["name"] + + +def get_version() -> str: + """ + Get project version defined in pyproject.toml + """ + return _open_pyproject()["tool"]["poetry"]["version"] + + +def get_description() -> str: + """ + Get project description defined in pyproject.toml + """ + return _open_pyproject()["tool"]["poetry"]["description"] diff --git a/src/utils/delay.ts b/src/utils/delay.ts deleted file mode 100644 index 5b771360..00000000 --- a/src/utils/delay.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default async function delay (t: number) { - await new Promise(function (resolve) { - setTimeout(resolve, t) - }) -} \ No newline at end of file diff --git a/src/utils/downloader/__init__.py b/src/utils/downloader/__init__.py new file mode 100644 index 00000000..fbea67cb --- /dev/null +++ b/src/utils/downloader/__init__.py @@ -0,0 +1,30 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +__init__.py +""" +# pylint: disable=unused-import +from .zip_downloader import ZipDownloader +from .sha256_downloader import Sha256Downloader +from .sig_downloader import SigDownloader +from .pem_downloader import PemDownloader +from .beta_downloader import BetaDownloader diff --git a/src/utils/downloader/asset_downloader.py b/src/utils/downloader/asset_downloader.py new file mode 100644 index 00000000..db619b23 --- /dev/null +++ b/src/utils/downloader/asset_downloader.py @@ -0,0 +1,103 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +asset_downloader.py +""" +import os +import typing +from .stream_downloader import StreamDownloader + + +class AssetDownloader(StreamDownloader): + """ + Subclass of :class:`StreamDownloader` for versioned asset releases. + """ + + def __init__(self, url: str, destdir: str, write_mode: str): + super().__init__(url=url) + self.destdir = destdir + self.write_mode = write_mode + + @property + def destdir(self) -> str: + """Getter for destination dir where the downloaded file will be placed""" + self.debug(f"destdir::getter={self._destdir}") + return self._destdir + + @destdir.setter + def destdir(self, value): + """Setter for destination dir where the downloaded file will be placed""" + self.debug(f"destdir::setter={value}") + if not os.path.exists(value): + os.makedirs(value, exist_ok=True) + + self._destdir = value + + @property + def write_mode(self) -> str: + """Getter for write mode ('wb' or 'w')""" + self.debug(f"write_mode::getter={self._write_mode}") + return self._write_mode + + @write_mode.setter + def write_mode(self, value: str): + """Setter for write mode ('wb' or 'w')""" + if value in ("w", "wb"): + self.debug(f"write_mode::setter={value}") + self._write_mode = value + else: + raise ValueError(f"Write Mode '{value}' not supported") + + def download(self, on_data: typing.Callable) -> str: + """ + Download some zip release given its version and put it + on a destination directory (default: OS temporary dir) + """ + + # Before the download the file stream, + # you can define some method to be called + # after the buffer is wrote + def local_on_data(data: bytes): + self.buffer.write(data) + on_data(data) + + self.on_data = local_on_data + self.download_file_stream(url=self.url) + + # Once the data is downloaded, you can + # put it on a file + destfile = os.path.join(self.destdir, self.filename) + self.debug(f"download::destfile={destfile}") + self.debug(f"download::write::{self.write_mode}={self.buffer.getvalue()}") + + if self.write_mode == "wb": + # pylint: disable=unspecified-encoding + with open(destfile, self.write_mode) as file: + file.write(self.buffer.getvalue()) + + if self._write_mode == "w": + with open(destfile, self.write_mode, encoding="utf8") as file: + value = self.buffer.getvalue() + text = value.decode("utf8") + file.write(text) + + return destfile diff --git a/src/utils/downloader/base_downloader.py b/src/utils/downloader/base_downloader.py new file mode 100644 index 00000000..894a7d5c --- /dev/null +++ b/src/utils/downloader/base_downloader.py @@ -0,0 +1,61 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +trigger_downloader.py +""" +import re +from io import BytesIO +from ..trigger import Trigger + + +class BaseDownloader(Trigger): + """ + Base class for downloader + """ + + REGEXP = r"https:\/\/(raw\.)?github(usercontent)?\.com\/(selfcustody|odudex)\/krux(\_binaries)?" + + def __init__(self, url: str): + super().__init__() + self._buffer = BytesIO() + self.url = url + + @property + def buffer(self) -> BytesIO: + """Getter for the buffer of the file to be downloaded""" + self.debug(f"buffer::getter={self._buffer}") + return self._buffer + + @property + def url(self) -> str: + """The asset's url to be downloaded""" + self.debug(f"url::getter={self._url}") + return self._url + + @url.setter + def url(self, value: str): + """The asset's url to be downloaded""" + if re.findall(BaseDownloader.REGEXP, value): + self.debug(f"url::setter={value}") + self._url = value + else: + raise ValueError(f"Invalid url: {value}") diff --git a/src/utils/downloader/beta_downloader.py b/src/utils/downloader/beta_downloader.py new file mode 100644 index 00000000..67d08a06 --- /dev/null +++ b/src/utils/downloader/beta_downloader.py @@ -0,0 +1,76 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +beta_downloader.py +""" +import tempfile +from .asset_downloader import AssetDownloader + + +class BetaDownloader(AssetDownloader): + """Download beta assets from odudex/krux_binaries""" + + VALID_DEVICES = ("m5stickv", "amigo", "dock", "bit", "yahboom", "cube", "wonder_mv") + + VALID_BINARY_TYPES = ("firmware.bin", "kboot.kfpkg") + + def __init__( + self, + device: str, + binary_type: str, + destdir: str = tempfile.gettempdir(), + ): + base_url = "https://raw.githubusercontent.com/odudex/krux_binaries/main" + url = f"{base_url}/maixpy_{device}/{binary_type}" + super().__init__(url=url, destdir=destdir, write_mode="wb") + self.device = device + self.binary_type = binary_type + + @property + def device(self): + """Getter for the device of beta version""" + self.debug(f"device::getter={self._device}") + return self._device + + @device.setter + def device(self, value: str): + """Setter for the device of beta version""" + if value in BetaDownloader.VALID_DEVICES: + self.debug(f"device::setter={value}") + self._device = value + else: + raise ValueError(f"Invalid device {value}") + + @property + def binary_type(self): + """Getter for the binary_type of beta version to be downloaded (firmware or kboot)""" + self.debug(f"binary_type::getter={self._binary_type}") + return self._binary_type + + @binary_type.setter + def binary_type(self, value: str): + """Setter for the binary_type of beta version to be downloaded (firmware or kboot)""" + if value in BetaDownloader.VALID_BINARY_TYPES: + self.debug(f"binary::setter={value}") + self._binary_type = value + else: + raise ValueError(f"Invalid binary_type {value}") diff --git a/src/utils/downloader/pem_downloader.py b/src/utils/downloader/pem_downloader.py new file mode 100644 index 00000000..230e26fb --- /dev/null +++ b/src/utils/downloader/pem_downloader.py @@ -0,0 +1,35 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +pem_downloader.py +""" +import tempfile +from .asset_downloader import AssetDownloader + + +class PemDownloader(AssetDownloader): + """Download .pem public certificate file""" + + def __init__(self, destdir: str = tempfile.gettempdir()): + base_url = "https://raw.githubusercontent.com/selfcustody/krux/main" + url = f"{base_url}/selfcustody.pem" + super().__init__(url=url, destdir=destdir, write_mode="w") diff --git a/src/utils/downloader/sha256_downloader.py b/src/utils/downloader/sha256_downloader.py new file mode 100644 index 00000000..5b3eb699 --- /dev/null +++ b/src/utils/downloader/sha256_downloader.py @@ -0,0 +1,35 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +sha256_downloader.py +""" +import tempfile +from .asset_downloader import AssetDownloader + + +class Sha256Downloader(AssetDownloader): + """Download .zip.sha256.txt release file""" + + def __init__(self, version: str, destdir: str = tempfile.gettempdir()): + base_url = "https://github.com/selfcustody/krux/releases/download" + url = f"{base_url}/{version}/krux-{version}.zip.sha256.txt" + super().__init__(url=url, destdir=destdir, write_mode="w") diff --git a/src/utils/downloader/sig_downloader.py b/src/utils/downloader/sig_downloader.py new file mode 100644 index 00000000..8039dba7 --- /dev/null +++ b/src/utils/downloader/sig_downloader.py @@ -0,0 +1,35 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +sig_downloader.py +""" +import tempfile +from .asset_downloader import AssetDownloader + + +class SigDownloader(AssetDownloader): + """Download .zip.sig release file""" + + def __init__(self, version: str, destdir: str = tempfile.gettempdir()): + base_url = "https://github.com/selfcustody/krux/releases/download" + url = f"{base_url}/{version}/krux-{version}.zip.sig" + super().__init__(url=url, destdir=destdir, write_mode="wb") diff --git a/src/utils/downloader/stream_downloader.py b/src/utils/downloader/stream_downloader.py new file mode 100644 index 00000000..757fbbe1 --- /dev/null +++ b/src/utils/downloader/stream_downloader.py @@ -0,0 +1,116 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2023 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +stream_downloader.py +""" +import os +import typing +import requests +from .trigger_downloader import TriggerDownloader + + +class StreamDownloader(TriggerDownloader): + """ + Download files in a stream mode + """ + + @property + def on_data(self) -> typing.Callable: + """Getter for callback to be used in each increment of downloaded stream""" + self.debug(f"on_data::getter={self._on_data}") + return self._on_data + + @on_data.setter + def on_data(self, value: typing.Callable): + """Setter for the callback to be used in each increment of downloaded stream""" + self.debug(f"on_data::setter={value}") + self._on_data = value + + def download_file_stream(self, url: str): + """ + Given a :attr:`url`, download a large file in a streaming manner to given + destination folder (:attr:`dest_dir`) + + When a chunk of received data is write to buffer, you can intercept + some information with :attr:`on_data` as function (total_len, downloaded_len, start_time) + until reaches the 100%. + + Then return the name + """ + # Get the filename by url and construct the request + # Check for any HTTPError and then process chunks of data + try: + self.filename = os.path.basename(url) + self.debug(f"download_file_stream::filename={self.filename}") + + headers = { + "Content-Disposition": f"attachment filename={self.filename}", + "Connection": "keep-alive", + "Cache-Control": "max-age=0", + "Accept-Encoding": "gzip, deflate, br", + } + self.debug( + "download_file_stream::requests.get=< url: " + + f"{url}, stream: True, headers: {headers}, timeout: 30 >" + ) + res = requests.get(url=url, stream=True, headers=headers, timeout=30) + + self.debug("download_file_stream::raise_for_status") + res.raise_for_status() + + except requests.exceptions.Timeout as t_exc: + raise RuntimeError(f"Download timeout error: {t_exc.__cause__ }") from t_exc + + except requests.exceptions.ConnectionError as c_exc: + raise RuntimeError( + f"Download connection error: {c_exc.__cause__}" + ) from c_exc + + except requests.exceptions.HTTPError as h_exc: + raise RuntimeError( + f"HTTP error {res.status_code}: {h_exc.__cause__}" + ) from h_exc + + # get some contents to calculate the amount + # of downloaded data + content_len = res.headers.get("Content-Length") + if content_len: + self.content_len = int(content_len) + else: + raise RuntimeError(f"Empty Content-Length response for {url}") + + self.debug(f"download_file_stream::content_len={self.content_len}") + + # Get the chunks of bytes data + # and pass it to a post-processing + # method defined as `on_data` + for chunk in res.iter_content(chunk_size=self.chunk_size): + self.downloaded_len += len(chunk) + self.debug(f"download_file_stream::downloaded_len={self.downloaded_len}") + if self.on_data is not None: + self.on_data(data=chunk) # pylint: disable=not-callable + else: + raise RuntimeError("on_data cannot be empty") + + # Now you can close connection + self.debug("downloaded_file_stream::closing_connection") + res.close() diff --git a/src/utils/downloader/trigger_downloader.py b/src/utils/downloader/trigger_downloader.py new file mode 100644 index 00000000..e65046f6 --- /dev/null +++ b/src/utils/downloader/trigger_downloader.py @@ -0,0 +1,92 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +trigger_downloader.py +""" +from .base_downloader import BaseDownloader + + +class TriggerDownloader(BaseDownloader): + """ + Downloader with some configurations adds + """ + + def __init__(self, url: str): + super().__init__(url=url) + self._content_len = 0 + self._filename = "" + self._downloaded_len = 0 + self._chunk_size = 1024 + + @property + def content_len(self) -> int: + """Getter for the content's length of the file to be downloaded""" + self.debug(f"content_len::getter={self._content_len}") + return self._content_len + + @content_len.setter + def content_len(self, value: int): + """Setter for the content's length of the file to be downloaded""" + self._content_len = value + self.debug(f"content_len::setter={value}") + + @property + def filename(self) -> str: + """Getter for the downloaded filename""" + self.debug(f"filename::getter={self._filename}") + return self._filename + + @filename.setter + def filename(self, value: str): + """Setter for the downloaded filename""" + self._filename = value + self.debug(f"filename::setter={self._filename}") + + @property + def downloaded_len(self) -> int: + """Getter for the ammount of downloaded data""" + self.debug(f"downloaded_len::getter={self._downloaded_len}") + return self._downloaded_len + + @downloaded_len.setter + def downloaded_len(self, value: int): + """Setter for the ammount of downloaded data""" + self._downloaded_len = value + self.debug(f"downloaded_len:setter={self._downloaded_len}") + + @property + def chunk_size(self) -> int: + """Getter for the size of chunks on downloaded data""" + self.debug(f"chunk_size::getter={self._chunk_size}") + return self._chunk_size + + @chunk_size.setter + def chunk_size(self, value: int): + """Setter for the size of chunks on downloaded data""" + # How to check if a given number is a power of two + # see + # https://stackoverflow.com/questions/57025836/how-to-check-if-a-given-number-is-a-power-of-two#57025941 + if (value & (value - 1) == 0) and value != 0: + self.debug(f"chunk_size::setter={value}") + self._chunk_size = value + else: + raise ValueError(f"{value} isnt a power of 2") diff --git a/src/utils/downloader/zip_downloader.py b/src/utils/downloader/zip_downloader.py new file mode 100644 index 00000000..147ad98c --- /dev/null +++ b/src/utils/downloader/zip_downloader.py @@ -0,0 +1,35 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +zip_downloader.py +""" +import tempfile +from .asset_downloader import AssetDownloader + + +class ZipDownloader(AssetDownloader): + """Download .zip release file""" + + def __init__(self, version: str, destdir: str = tempfile.gettempdir()): + base_url = "https://github.com/selfcustody/krux/releases/download" + url = f"{base_url}/{version}/krux-{version}.zip" + super().__init__(url=url, destdir=destdir, write_mode="wb") diff --git a/src/utils/flasher/__init__.py b/src/utils/flasher/__init__.py new file mode 100644 index 00000000..9fb90818 --- /dev/null +++ b/src/utils/flasher/__init__.py @@ -0,0 +1,27 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +__init__.py +""" + +from .flasher import Flasher +from .wiper import Wiper diff --git a/src/utils/flasher/base_flasher.py b/src/utils/flasher/base_flasher.py new file mode 100644 index 00000000..e53c96be --- /dev/null +++ b/src/utils/flasher/base_flasher.py @@ -0,0 +1,167 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +base_flasher.py +""" +import os +import typing +from serial import Serial +from serial.serialutil import SerialException +from serial.tools import list_ports +from src.utils.trigger import Trigger +from src.utils.kboot.build.ktool import KTool + + +class BaseFlasher(Trigger): + """ + Base class to flash kboot.kfpkg on devices + """ + + VALID_BOARDS = ("goE", "dan") + VALID_BAUDRATES = ( + 9600, + 19200, + 28800, + 38400, + 57600, + 76800, + 115200, + 230400, + 460800, + 576000, + 921600, + 1500000, + ) + + def __init__(self): + super().__init__() + self.ktool = KTool() + + @property + def firmware(self) -> str: + """Getter for firmware's full path""" + self.debug(f"firmware::getter={self._firmware}") + return self._firmware + + @firmware.setter + def firmware(self, value: str): + """Setter for firmware's firmware's full path""" + if not os.path.exists(value): + raise ValueError(f"File do not exist: {value}") + + self.debug(f"firmware::setter={value}") + self._firmware = value + + @property + def port(self) -> str: + """Getter for device port system full path""" + self.debug(f"ports::getter={self._port}") + return self._port + + @port.setter + def port(self, value: str): + """Setter for available ports's full path by giving device name""" + if value in ("amigo", "amigo_tft", "amigo_ips", "m5stickv", "bit", "cube"): + vid = "0403" + + elif value in ("dock", "yahboom", "wonder_mv"): + vid = "7523" + + else: + raise ValueError(f"Device not implemented: {value}") + + self._available_ports_generator = list_ports.grep(vid) + port = next(self._available_ports_generator) + self._port = port.device + self.debug(f"ports::setter={self._port}") + + @property + def board(self) -> str: + """Return a new instance of board""" + self.debug(f"board::getter={self._board}") + return self._board + + @board.setter + def board(self, value: str): + """Setter for board giving device name""" + if value in ( + "amigo", + "amigo_tft", + "amigo_ips", + "m5stickv", + "bit", + "yahboom", + "cube", + ): + self._board = "goE" + self.debug(f"board::setter={self._board}") + + elif value in ("dock", "wonder_mv"): + self._board = "dan" + self.debug(f"board::setter={self._board}") + + else: + raise ValueError(f"Device not implemented: {value}") + + @property + def baudrate(self) -> int: + """Getter for baudrate""" + self.debug(f"baudrate::getter={self._baudrate}") + return self._baudrate + + @baudrate.setter + def baudrate(self, value: int): + """Setter for baudrate""" + if value in BaseFlasher.VALID_BAUDRATES: + self.debug(f"baudrate::setter={value}") + self._baudrate = value + else: + raise ValueError(f"Invalid baudrate: {str(value)}") + + @property + def print_callback(self): + """ + Getter for print_callback. KTool have two callbacks: + - print_callback: a property of an instance of KTool that do KTool.log calls + - callback: an argument of `process` method that parse the progress of flash + """ + self.debug(f"print_callback::getter={self._print_callback}") + return self._print_callback + + @print_callback.setter + def print_callback(self, value: typing.Callable): + """ + Getter for print_callback. KTool have two callbacks: + - print_callback: a property of an instance of KTool that do KTool.log calls + - callback: an argument of `process` method that parse the progress of flash + """ + self.debug(f"print_callback::setter={value}") + self._print_callback = value + + def is_port_working(self, port) -> bool: + """Check if a port is working""" + try: + serialport = Serial(port) + serialport.close() + return True + except SerialException: + return False diff --git a/src/utils/flasher/flasher.py b/src/utils/flasher/flasher.py new file mode 100644 index 00000000..1d6ab1f3 --- /dev/null +++ b/src/utils/flasher/flasher.py @@ -0,0 +1,98 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +__init__.py +""" +import typing +from src.utils.selector import VALID_DEVICES +from src.utils.flasher.base_flasher import BaseFlasher + +# Example of parsing progress +# def get_progress(file_type_str, iteration, total, suffix): +# """Default callback for flashing (repeat the one from ktool)""" +# percent = ("{0:." + str(1) + "f}").format(100 * (iteration / float(total))) +# filled_length = int(100 * iteration // total) +# barascii = "=" * filled_length + "-" * (100 - filled_length) +# msg = f"\r%|{barascii}| {percent}% {suffix}" +# if percent == 100: +# print() +# print(msg) +# else: +# sys.stdout.write(msg) + + +class Flasher(BaseFlasher): + """ + A class to parse KTool outputs: We don't want to modify the + KTool structure, instead, only redirect what happens in + :attr:`KTool.process`. + """ + + def flash(self, callback: typing.Callable): + """ + Detect available ports, try default flash process and + if not work, try custom port + """ + for device in VALID_DEVICES: + # pylint: disable=unsupported-membership-test + if device in self.firmware: + self.port = device + self.board = device + + if self.is_port_working(self.port): + try: + self.ktool.process( + terminal=False, + dev=self.port, + baudrate=int(self.baudrate), + board=self.board, + file=self.firmware, + callback=callback, + ) + + # pylint: disable=broad-exception-caught + except Exception as exc: + self.ktool.__class__.log(f"{str(exc)} for {self.port}") + self.ktool.__class__.log("") + + try: + newport = next(self._available_ports_generator) + if self.is_port_working(newport.device): + self.ktool.process( + terminal=False, + dev=newport.device, + baudrate=int(self.baudrate), + board=self.board, + file=self.firmware, + callback=callback, + ) + + else: + exc = RuntimeError(f"Port {newport.device} not working") + self.ktool.__class__.log(str(exc)) + + except StopIteration as stop_exc: + self.ktool.__class__.log(str(stop_exc)) + + else: + exc = RuntimeError(f"Port {self.port} not working") + self.ktool.__class__.log(str(exc)) diff --git a/src/utils/flasher/wiper.py b/src/utils/flasher/wiper.py new file mode 100644 index 00000000..4fd84532 --- /dev/null +++ b/src/utils/flasher/wiper.py @@ -0,0 +1,78 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +wiper.py +""" +import sys +from src.utils.flasher.base_flasher import BaseFlasher +from src.utils.selector import VALID_DEVICES + + +class Wiper(BaseFlasher): + """Class to wipe some specific board""" + + def wipe(self, device: str): + """Detect available ports, try default erase process and + it not work, try custom port""" + for dev in VALID_DEVICES: + if dev == device: + self.info(f"Detected valid {device} to be wiped") + self.port = device + self.board = device + + if self.is_port_working(self.port): + try: + sys.argv.extend( + ["-B", self.board, "-b", str(self.baudrate), "-p", self.port, "-E"] + ) + + self.ktool.process() + + # pylint: disable=broad-exception-caught + except Exception as exc: + self.ktool.__class__.log(f"{str(exc)} for {self.port}") + self.ktool.__class__.log("") + + try: + newport = next(self._available_ports_generator) + if self.is_port_working(newport.device): + sys.argv = [ + "--port", + newport.device, + "--Board", + self.board, + "--baudrate", + str(self.baudrate), + "-E", + ] + self.ktool.process() + + else: + exc = RuntimeError(f"Port {newport.device} not working") + self.ktool.__class__.log(str(exc)) + + except StopIteration as stop_exc: + self.ktool.__class__.log(str(stop_exc)) + + else: + exc = RuntimeError(f"Port {self.port} not working") + self.ktool.__class__.log(str(exc)) diff --git a/src/utils/info/__init__.py b/src/utils/info/__init__.py new file mode 100644 index 00000000..8cbe5236 --- /dev/null +++ b/src/utils/info/__init__.py @@ -0,0 +1,83 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2023 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +info.py +""" +from inspect import currentframe, unwrap + + +def mro(): + """ + we can loop through the self's MRO and try to find the method + that called us. + + :see: https://stackoverflow.com/questions/53153075/get-a-class-name-of-calling-method + """ + + # get the call frame of the calling method + frame = currentframe().f_back + try: + # find the name of the first variable in the calling + # function - which is hopefully the "self" + codeobj = frame.f_code + try: + self_name = codeobj.co_varnames[0] + except IndexError: + return None + + # try to access the caller's "self" + try: + self_obj = frame.f_locals[self_name] + except KeyError: + return None + + # check if the calling function is really a method7 + self_type = type(self_obj) + func_name = codeobj.co_name + + # iterate through all classes in the MRO + for cls in self_type.__mro__: + # see if this class has a method with the name + # we're looking for + try: + method = vars(cls)[func_name] + + except KeyError: + continue + + # unwrap the method just in case there are any decorators + try: + method = unwrap(method) + except ValueError: + pass + + # see if this is the method that called us + if getattr(method, "__code__", None) is codeobj: + name = self_type.__name__ + return name + + # if we didn't find a matching method, return None + return None + + finally: + # make sure to clean up the frame at the end to avoid ref cycles + del frame diff --git a/src/utils/kboot b/src/utils/kboot new file mode 160000 index 00000000..78348888 --- /dev/null +++ b/src/utils/kboot @@ -0,0 +1 @@ +Subproject commit 78348888ef0275312efc4e68b51755a8167ac11f diff --git a/src/utils/messages.ts b/src/utils/messages.ts deleted file mode 100644 index 480c9704..00000000 --- a/src/utils/messages.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Ref } from "vue" -import delay from "./delay" - -async function add( - data: Ref>, - message: string -): Promise { - data.value.messages.push(message) - data.value.indexes.push(0) - await delay(10) - data.value.indexes[data.value.indexes.length - 1] += 1 - await delay(3000) -} - -function clean(data: Ref>): void { - data.value.messages = [] - data.value.indexes = [] - data.value.output = "" - data.value.done = false -} - -async function close(data: Ref>): Promise { - for(let i in data.value.indexes) { - data.value.indexes[i] = 2 - await delay(100) - } -} - -export default { add, clean, close } diff --git a/src/utils/onError.ts b/src/utils/onError.ts deleted file mode 100644 index 3077178b..00000000 --- a/src/utils/onError.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Ref } from "vue" - -/** - * Generalized Procedure to redirects to ErrorMsg page - * @param _ - * @param result - */ -export default function (data: Ref>): Function { - return async function (_: Event, result: Record<'name' | 'message' | 'stack', any>): Promise { - data.value = { - ...result, - backTo: 'Main' - } - if (data.value.output) { - data.value.output = "" - } - await window.api.invoke('krux:change:page', { page: 'ErrorMsg' }) - } -} diff --git a/src/utils/onKruxChangePage.ts b/src/utils/onKruxChangePage.ts deleted file mode 100644 index 923b52ca..00000000 --- a/src/utils/onKruxChangePage.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Ref } from "vue" - -/** - * Simple function to change pages - * @param page - * @returns Function - */ -export default function ( - data: Ref>, - page: Ref -): Function { - return function (_: Event, result: Record<'from' | 'page', string>) { - if (result.from === 'KruxInstallerLogo') { - delete data.value.messages - delete data.value.indexes - } - page.value = result.page - } -} \ No newline at end of file diff --git a/src/utils/onKruxCheckIfItWillFlashHandler.ts b/src/utils/onKruxCheckIfItWillFlashHandler.ts deleted file mode 100644 index b3f2c2d1..00000000 --- a/src/utils/onKruxCheckIfItWillFlashHandler.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Ref } from "vue" - -function wipeOrFlash(data: Ref>, kind: string): string { - let click = '' - if (data.value.os === 'linux') { - click = `${kind} with ktool-linux` - } - else if (data.value.os === 'win32') { - click = `${kind} with ktool-win.exe` - } - else if (data.value.os === 'darwin' && !data.value.isMac10) { - click = `${kind} with ktool-mac` - } - else if (data.value.os === 'darwin' && data.value.isMac10) { - click = `${kind} with ktool-mac-10` - } - return click -} - -export default function (data: Ref>): Function { - return function (_: Event, result: Record<'showFlash', boolean>): void { - data.value.showFlash = result.showFlash - if (!data.value.showFlash && data.value.device !== 'Select device') { - if (data.value.version === 'Select version') { - if(data.value.showWipe) { - const click = wipeOrFlash(data, 'Wipe') - data.value.clickMessage = `Click 'Select version' or '${click}'` - } else { - data.value.clickMessage = `Click 'Select version'` - } - } else { - const click = wipeOrFlash(data, 'Wipe') - data.value.clickMessage = `Click 'Version: ${data.value.version}' or ${click} for ${data.value.device}` - } - } else { - const click = wipeOrFlash(data, 'Wipe/Flash') - data.value.clickMessage = `Connect your ${data.value.device} device and power on it before click '${click}'` - } - } -} diff --git a/src/utils/onKruxCheckIfItWillWipeHandler.ts b/src/utils/onKruxCheckIfItWillWipeHandler.ts deleted file mode 100644 index fec6707d..00000000 --- a/src/utils/onKruxCheckIfItWillWipeHandler.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Ref } from "vue" - -function wipeOrFlash(data: Ref>, kind: string): string { - let click = '' - if (data.value.os === 'linux') { - click = `${kind} with ktool-linux` - } - else if (data.value.os === 'win32') { - click = `${kind} with ktool-win.exe` - } - else if (data.value.os === 'darwin' && !data.value.isMac10) { - click = `${kind} with ktool-mac` - } - else if (data.value.os === 'darwin' && data.value.isMac10) { - click = `${kind} with ktool-mac-10` - } - return click -} - -export default function (data: Ref>): Function { - return function (_: Event, result: Record<'showWipe', boolean>): void { - data.value.showWipe = result.showWipe - } -} diff --git a/src/utils/onKruxCheckResources.ts b/src/utils/onKruxCheckResources.ts deleted file mode 100644 index 01262213..00000000 --- a/src/utils/onKruxCheckResources.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { Ref } from "vue" -import messages from "./messages" - -async function onResourceExist ( - data: Ref>, - result: Record<'from' | 'exists' | 'baseUrl' | 'resourceFrom' | 'resourceTo', any> -) { - let checked - if (result.resourceTo.match(/^.*(zip|sha256.txt|sig|pem)$/g)){ - checked = result.resourceTo.split('krux-installer/')[1] - // on Windows, the path came with inverted slashes, - // so, if the checking fails, - // check again - if (checked === undefined) { - checked = result.resourceTo.split('krux-installer\\')[1] - } - } else if (result.resourceTo.match(/^.*(firmware|kboot|ktool).*$/g)) { - checked = result.resourceTo.split('/main/')[1] - // on Windows, the path came with inverted slashes, - // so, if the checking fails, - // check again - if (checked === undefined) { - checked = result.resourceTo.split('\\main\\')[1] - } - } - await messages.add(data, `${checked} found`) - data.value.proceedTo = 'ConsoleLoad' - data.value.backTo = 'GithubChecker' - await messages.close(data) - await window.api.invoke('krux:change:page', { page: 'WarningDownload' }) -} - -async function onResourceNotExist ( - data: Ref>, - result: Record<'from' | 'exists' | 'baseUrl' | 'resourceFrom' | 'resourceTo', any>, - page: string -) { - let checked - if (result.resourceTo.match(/^.*(zip|sha256.txt|sig|pem)$/g)){ - checked = result.resourceTo.split('krux-installer/')[1] - // on Windows, the path came with inverted slashes, - // so, if the checking fails, - // check again - if (checked === undefined) { - checked = result.resourceTo.split('krux-installer\\')[1] - } - } else if (result.resourceTo.match(/^.*(firmware|kboot|ktool).*$/g)) { - checked = result.resourceTo.split('/main/')[1] - // on Windows, the path came with inverted slashes, - // so, if the checking fails, - // check again - if (checked === undefined) { - checked = result.resourceTo.split('\\main\\')[1] - } - } - await messages.add(data, `${checked} not found`) - data.value.progress = 0.0 - await messages.close(data) - await window.api.invoke('krux:change:page', { page: page }) -} - -async function onDownloadAgain ( - data: Ref>, - result: Record<'from' | 'exists' | 'baseUrl' | 'resourceFrom' | 'resourceTo', any>, - page: string -) { - data.value.progress = 0.0 - await messages.close(data) - await window.api.invoke('krux:change:page', { page: page }) -} - -export default function (data: Ref>): Function { - return async function ( - _: Event, - result: Record<'from' | 'exists' | 'baseUrl' | 'resourceFrom' | 'resourceTo', any> - ): Promise { - data.value.baseUrl = result.baseUrl - data.value.resourceFrom = result.resourceFrom - data.value.resourceTo = result.resourceTo - - // When user decides between official - // or test releaases - if (result.from === 'SelectVersion' ) { - if (result.exists) { - await onResourceExist(data, result) - } else { - if (result.baseUrl.match(/selfcustody/g)) { - await onResourceNotExist(data, result, 'DownloadOfficialReleaseZip') - } - if (result.resourceFrom.match(/odudex\/krux_binaries/g)){ - await onResourceNotExist(data, result, 'DownloadTestFirmware') - } - } - } - - - // When user decides for official release - // and checked zip file to redirect to sha256.txt file - if ( - result.from === 'DownloadOfficialReleaseZip' || - result.from.match(/^WarningDownload::.*.zip$/) - ) { - if (result.exists) { - await onResourceExist(data, result) - } else { - await onResourceNotExist(data, result, 'DownloadOfficialReleaseSha256') - } - } - - // When user decides for official release - // and checked zip.sha256.txt file to redirect to zip.sig file - if ( - result.from === 'DownloadOfficialReleaseSha256' || - result.from.match(/^WarningDownload::.*.zip.sha256.txt$/) - ) { - if (result.exists) { - await onResourceExist(data, result) - } else { - await onResourceNotExist(data, result, 'DownloadOfficialReleaseSig') - } - } - - // When user decides for official release - // and checked zip.sig file to redirect to .pem file - if ( - result.from === 'DownloadOfficialReleaseSig' || - result.from.match(/^WarningDownload::.*.zip.sig$/) - ) { - if (result.exists) { - await onResourceExist(data, result) - } else { - await onResourceNotExist(data, result, 'DownloadOfficialReleasePem') - } - } - - if (result.from === 'CheckVerifyOfficialRelease') { - data.value = {} - } - - if ( - result.from === 'DownloadTestFirmware' || - result.from.match(/^WarningDownload::.*firmware.bin$/) - ) { - if (result.exists) { - await onResourceExist(data, result) - } else { - await onResourceNotExist(data, result, 'DownloadTestKboot') - } - } - - if ( - result.from === 'DownloadTestKboot' || - result.from.match(/^WarningDownload::.*kboot.kfpkg$/) - ) { - if (result.exists) { - await onResourceExist(data, result) - } else { - await onResourceNotExist(data, result, 'DownloadTestKtool') - } - } - - if ( - result.from === 'DownloadTestKtool' || - result.from.match(/^WarningDownload::.*ktool-(linux|win.exe|mac|mac-10)$/) - ) { - if (result.exists) { - await onResourceExist(data, result) - } else { - await onResourceNotExist(data, result, 'Main') - } - } - - // ==================================================== - // Warning Cycle: when user decide to re-download files - // ==================================================== - - if (result.from.match(/^Again::WarningDownload::.*.zip$$/)) { - await onDownloadAgain(data, result, 'DownloadOfficialReleaseZip') - } - - if (result.from.match(/^Again::WarningDownload::.*.zip.sha256.txt$/)) { - await onDownloadAgain(data, result, 'DownloadOfficialReleaseSha256') - } - - if (result.from.match(/^Again::WarningDownload::.*.zip.sig$/)) { - await onDownloadAgain(data, result, 'DownloadOfficialReleaseSig') - } - - if (result.from.match(/^Again::WarningDownload::.*.pem$/)) { - await onDownloadAgain(data, result, 'DownloadOfficialReleasePem') - } - - if (result.from.match(/^Again::WarningDownload::.*firmware.bin$/)) { - await onDownloadAgain(data, result, 'DownloadTestFirmware') - } - - if (result.from.match(/^Again::WarningDownload::.*kboot.kfpkg$/)) { - await onDownloadAgain(data, result, 'DownloadTestKboot') - } - - if (result.from.match(/^Again::WarningDownload::.*ktool-(linux|win.exe|mac|mac-10)$/)) { - await onDownloadAgain(data, result, 'DownloadTestKtool') - } - - if (result.from.match(/^CheckShowFlash::Main::selfcustody$/)) { - data.value.showFlash = !data.value.showFlash && result.exists - } - - if (result.from.match(/^CheckShowFlash::Main::odudex::firmware$/)) { - data.value.showFlash = !data.value.showFlash && result.exists - } - } -} \ No newline at end of file diff --git a/src/utils/onKruxDownloadResources.ts b/src/utils/onKruxDownloadResources.ts deleted file mode 100644 index 4ae30a40..00000000 --- a/src/utils/onKruxDownloadResources.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Ref } from "vue" -import delay from "./delay" - -/** - * When download finishes, redirect it: - * - DownloadOfficialReleaseZip --> CheckResourcesOfficialReleaseSha256 - * - DownloadOfficialReleaseSha256 --> CheckResourcesOfficialReleaseSig - * - DownloadOfficialReleaseSig --> CheckResourcesOfficialReleasePem - * - DownloadOfficialReleasePem --> VerifyOfficialRelease - * - DownloadTestFirmware --> CheckResourcesTestKboot - * - DownloadTestKboot --> CheckResourcesTestKtool - * - DownloadTestKtool --> Main - * @param data - * @returns Function - */ -export default function (data: Ref>): Function { - return async function (_: Event, result: Record<'from', string>): Promise { - await delay(3000) - await window.api.invoke('krux:store:get', { - from: result.from, - keys: ['device', 'version', 'os', 'isMac10'] - }) - } -} \ No newline at end of file diff --git a/src/utils/onKruxDownloadResourcesData.ts b/src/utils/onKruxDownloadResourcesData.ts deleted file mode 100644 index 5a1e0da2..00000000 --- a/src/utils/onKruxDownloadResourcesData.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Ref } from "vue" - -/** - * Stream progress when download resources - * - * @param data - * @returns Function - */ -export default function (data: Ref>): Function { - return function (_: Event, result: string) { - data.value.progress = result - } -} \ No newline at end of file diff --git a/src/utils/onKruxFlash.ts b/src/utils/onKruxFlash.ts deleted file mode 100644 index d67c94fd..00000000 --- a/src/utils/onKruxFlash.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Ref } from "vue"; - -export default function ( - data: Ref> -): Function { - return function ( - _: Event, - result:Record - ): void { - data.value.done = result.done - data.value.output = "" - } -} diff --git a/src/utils/onKruxFlashData.ts b/src/utils/onKruxFlashData.ts deleted file mode 100644 index 79df67a9..00000000 --- a/src/utils/onKruxFlashData.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Ref } from "vue"; - -// If you have no problems simply ignoring all type-checking features for this library, you have two options: -// Add @ts-ignore above all imports or Create a declaration file with any type, so all imports are automatically considered to be of any type. -// see https://stackoverflow.com/questions/56688893/how-to-use-a-module-when-it-could-not-find-a-declaration-file#answer-56690386 -// @ts-ignore -import { AnsiUp } from 'ansi_up' - -/** - * Stream shell output to web frontend - * @see https://www.appsloveworld.com/vuejs/100/8/stream-shell-output-to-web-front-end - * @param data - */ -export default function ( - data: Ref> -): Function { - return function ( - _: Event, - result:string - ): void { - const ansi = new AnsiUp() - let tmp = result.replace(/%\s/, "\n") - tmp = tmp.replace(/kiB\/s/g, "kiB/s\n") - - data.value.output = ansi.ansi_to_html(tmp).replace(/\n/gm, '
') - } -} diff --git a/src/utils/onKruxStoreGet.ts b/src/utils/onKruxStoreGet.ts deleted file mode 100644 index 65e29f26..00000000 --- a/src/utils/onKruxStoreGet.ts +++ /dev/null @@ -1,302 +0,0 @@ -import { Ref } from "vue" -import setMainData from './setMainData' -import messages from "./messages" - -async function onGetResource ( - data: Ref>, - result: Record, - options: Record -): Promise { - - let toCheck = '' - if (options.resource.match(/^.*(zip|sha256.txt|sig|pem)$/g)){ - toCheck = options.resource.match(/^.*(zip|sha256.txt|sig|pem)$/g)[0] - } else if (options.resource.match(/^.*(firmware|kboot|ktool).*$/g)) { - toCheck = options.resource.split('/main/')[1] - } - - await messages.add(data, `Checking ${toCheck}`) - await window.api.invoke('krux:check:resource', { - from: result.from, - baseUrl: options.baseUrl, - resource: options.resource - }) -} - -/** - * Setup `data` and/or redirects to a `page`, or invoke another api call; - * - * ### KruxInstallerLogo - * - * - When user start app - * - * ### Main page - * - * When `result.from` value is: - * - KruxInstallerLogo; - * - SelectVersion; - * - VerifiedOfficialRelease; or - * - ErrorMsg - * - * Set the `data` variable the following properties: - * - `device`; - * - `version`; and - * - `ktool`. - * - * ### SelectDevice - * - * When user selected device to be flashed; - * - * It will need to decide wheather go to official release - * or odudex's experimental release. After,it will need to check if the specific resource exist. - * If not exist, redirect to download pages; if exist, redirect to warning page. - * - * The likely cycles will be: - * - * * Official release: - * * SelectVersion - * * krux:check:resource for https://github.com/selfcustody/krux/releases/download/{{ version }}/krux-{{ version }}.zip: - * * if exists, then krux:change:page to WarningDownload: - * * proceed to krux:check:resource for https://github.com/selfcustody/krux/releases/download/{{ version }}/krux-{{ version }}.zip.sha256.txt - * * download again with krux:change:page to DownloadOfficialReleaseZip - * * back to krux:change:page GithubChecker - * * if not exists, then krux:change:page to DownloadOfficialReleaseZip - * * krux:check:resource for https://github.com/selfcustody/krux/releases/download/{{ version }}/krux-{{ version }}.zip.sha256.txt: - * * if exists, then krux:change:page to WarningDownload: - * * proceed to krux:check:resource for https://github.com/selfcustody/krux/releases/download/{{ version }}/krux-{{ version }}.zip.sig - * * download again with krux:change:page to DownloadOfficialReleaseSha256 - * * back to krux:change:page GithubChecker - * * if not exists, then krux:change:page to DownloadOfficialReleaseSha256 - * * krux:check:resource for https://github.com/selfcustody/krux/releases/download/{{ version }}/krux-{{ version }}.zip.sig: - * * if exists, then krux:change:page to WarningDownload: - * * proceed to krux:check:resource for https://raw.githubusercontent.com/selfcustody/krux/main/selfcustody.pem - * * download again with krux:change:page to DownloadOfficialReleaseSig - * * back to krux:change:page GithubChecker - * * if not exists, then krux:change:page to DownloadOfficialReleasePem - * * krux:check:resource for https://raw.githubusercontent.com/selfcustody/krux/main/selfcustody.pem: - * * if exists, then krux:change:page to WarningDownload: - * * proceed to krux:change:page for CheckVerifyOfficialRelease - * * download again with krux:change:page to DownloadOfficialReleasePem - * * back to krux:change:page GithubChecker - * * if not exists, then krux:change:page to DownloadOfficialReleasePem - * - * @param data - * @param options - * @returns Promise - */ -export default function onKruxStoreGet (data: Ref>): Function { - return async function (_: Event, result: Record<'from' | 'key' | 'values', any>): Promise { - - // When user start app - if ( result.from === 'KruxInstallerLogo') { - data.value = {} - messages.clean(data) - await window.api.invoke('krux:change:page', { page: 'ConsoleLoad' }) - await messages.add(data, 'Loading data from storage') - await messages.add(data, 'Verifying openssl') - await window.api.invoke('krux:verify:openssl', { from: 'KruxInstallerLogo' }) - setMainData(data, result) - } - - // - - // When user selected device to be flashed - if (result.from === 'SelectDevice') { - await setMainData(data, result) - await window.api.invoke('krux:change:page', { page: 'Main' }) - } - - // When user selected back on SelectVersion - if (result.from === 'Back::SelectVersion' ) { - await setMainData(data, result) - await window.api.invoke('krux:change:page', { page: 'Main' }) - } - - // When user selected between - // official release version (.zip -> .zip.sha256.txt -> .zip.sig -> .pem files) - // or test (.bin -> .kboot -> .kfpkg -> ktool) - if (result.from === 'SelectVersion') { - messages.clean(data) - await window.api.invoke('krux:change:page', { page: 'ConsoleLoad' }) - - // official release version (.zip -> .zip.sha256.txt -> .zip.sig -> .pem files) - if (result.values.version.match(/selfcustody\/.*/g)){ - - const domain = 'https://github.com' - let baseUrl = result.values.version.replace(/tag/g, 'download') - let version = baseUrl.split('download/')[1] - baseUrl = baseUrl.split(`/${version}`)[0] - - await onGetResource(data, result, { - baseUrl: `${domain}/${baseUrl}`, - resource: `${version}/krux-${version}.zip` - }) - } - - // or test (.bin -> .kboot -> .kfpkg -> ktool) - if (result.values.version.match(/odudex\/krux_binaries/g)){ - - await onGetResource(data, result, { - baseUrl: 'https://raw.githubusercontent.com', - resource: `${result.values.version}/main/${result.values.device}/firmware.bin` - }) - - } - setMainData(data, result) - } - - // =========================== - // Official release life cycle - // =========================== - - // When user came from: - // * .zip file - // * chacked it (and exists) - // * check for .zip.sha256.txt file - if ( - result.from === 'DownloadOfficialReleaseZip' || - result.from.match(/^WarningDownload::.*.zip$/) - ) { - await setMainData(data, result) - messages.clean(data) - await window.api.invoke('krux:change:page', { page: 'ConsoleLoad' }) - - const domain = 'https://github.com' - let baseUrl = result.values.version.replace(/tag/g, 'download') - let version = baseUrl.split('download/')[1] - baseUrl = baseUrl.split(`/${version}`)[0] - - await onGetResource(data, result, { - baseUrl: `${domain}/${baseUrl}`, - resource: `${version}/krux-${version}.zip.sha256.txt` - }) - } - - // When user came from .zip.sha256.txt file and will check for .zip.sig file - if ( - result.from === 'DownloadOfficialReleaseSha256' || - result.from.match(/^WarningDownload::.*.zip.sha256.txt$/) - ) { - await setMainData(data, result) - messages.clean(data) - await window.api.invoke('krux:change:page', { page: 'ConsoleLoad' }) - - const domain = 'https://github.com' - let baseUrl = result.values.version.replace(/tag/g, 'download') - let version = baseUrl.split('download/')[1] - baseUrl = baseUrl.split(`/${version}`)[0] - - await onGetResource(data, result, { - baseUrl: `${domain}/${baseUrl}`, - resource: `${version}/krux-${version}.zip.sig` - }) - } - - // When user came from .zip.sig file and will check for .pem file - if ( - result.from === 'DownloadOfficialReleaseSig' || - result.from.match(/^WarningDownload::.*.zip.sig$/) - ) { - await setMainData(data, result) - messages.clean(data) - await window.api.invoke('krux:change:page', { page: 'ConsoleLoad' }) - - await onGetResource(data, result, { - baseUrl: 'https://raw.githubusercontent.com/selfcustody/krux', - resource: 'main/selfcustody.pem' - }) - } - - // When user came from .zip.sig file and will check for .pem file - if ( - result.from === 'DownloadOfficialReleasePem' || - result.from.match(/^WarningDownload::.*.pem$/) - ) { - await setMainData(data, result) - messages.clean(data) - await window.api.invoke('krux:change:page', { page: 'CheckVerifyOfficialRelease' }) - } - - if (result.from === 'CheckVerifyOfficialRelease') { - await window.api.invoke('krux:verify:releases:hash') - } - - if ( result.from === 'VerifiedOfficialRelease') { - setMainData(data, result) - } - - // ======================= - // Test release life cycle - // ======================= - - if ( - result.from === 'DownloadTestFirmware' || - result.from.match(/^WarningDownload::.*firmware.bin$/) - ) { - await setMainData(data, result) - messages.clean(data) - await window.api.invoke('krux:change:page', { page: 'ConsoleLoad' }) - - await onGetResource(data, result, { - baseUrl: 'https://raw.githubusercontent.com', - resource: `${result.values.version}/main/${result.values.device}/kboot.kfpkg` - }) - - } - - if ( - result.from === 'DownloadTestKboot' || - result.from.match(/^WarningDownload::.*kboot.kfpkg$/) - ) { - await setMainData(data, result) - messages.clean(data) - await window.api.invoke('krux:change:page', { page: 'ConsoleLoad' }) - - let resource = '' - - if (result.values.os === 'linux') { - resource = `main/ktool-linux` - } - else if (result.values.os === 'win32') { - resource = `main/ktool-win.exe` - } - else if (result.values.os === 'darwin' && !result.values.isMac10) { - resource = `main/ktool-mac` - } - else if (result.values.os === 'darwin' && result.values.isMac10) { - resource = `main/ktool-mac-10` - } - - await onGetResource(data, result, { - baseUrl: 'https://raw.githubusercontent.com', - resource: `${result.values.version}/${resource}` - }) - } - - if ( - result.from === 'DownloadTestKtool' || - result.from.match(/^WarningDownload::.*ktool-(linux|win.exe|mac|mac-10)$/) - ) { - await setMainData(data, result) - messages.clean(data) - await window.api.invoke('krux:change:page', { page: 'Main' }) - } - - if (result.from === 'Back::WarningDownload') { - await setMainData(data, result) - messages.clean(data) - await window.api.invoke('krux:change:page', { page: 'Main' }) - } - - if ( result.from === 'ErrorMsg') { - await setMainData(data, result) - } - - if (result.from === 'FlashToDevice' || result.from === 'WipeDevice') { - messages.clean(data) - await window.api.invoke('krux:change:page', { page: 'Main' }) - setMainData(data, result) - } - - } -} diff --git a/src/utils/onKruxStoreSet.ts b/src/utils/onKruxStoreSet.ts deleted file mode 100644 index 2a9924e6..00000000 --- a/src/utils/onKruxStoreSet.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Ref } from "vue" - -/** - * Setup `data` for SelectDevice and SelectVersion Pages - * @param data - * @returns - */ -export default function (data: Ref>): Function { - return async function (_: Event, result: Record<'from' | 'key' | 'value', any>) { - if (result.from === 'SelectDevice' || result.from === 'SelectVersion') { - await window.api.invoke('krux:store:get', { from: result.from, keys: ['device', 'version', 'os', 'isMac10'] }) - } - } -} \ No newline at end of file diff --git a/src/utils/onKruxUnzip.ts b/src/utils/onKruxUnzip.ts deleted file mode 100644 index 7b0d586f..00000000 --- a/src/utils/onKruxUnzip.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Ref } from "vue"; - -/** - * Stream shell output to web frontend - * @see https://www.appsloveworld.com/vuejs/100/8/stream-shell-output-to-web-front-end - * @param data - */ -export default function ( - data: Ref> -): Function { - return async function ( - _: Event, - result:Record<'will', string> - ): Promise{ - console.log("DATA===============") - console.log(data) - console.log("RESULT===============") - console.log(result) - if (result.will == 'flash') { - await window.api.invoke('krux:flash') - } else if (result.will == 'wipe') { - await window.api.invoke('krux:wipe') - } - } -} diff --git a/src/utils/onKruxUnzipData.ts b/src/utils/onKruxUnzipData.ts deleted file mode 100644 index 8f35b324..00000000 --- a/src/utils/onKruxUnzipData.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Ref } from "vue"; - -/** - * Stream shell output to web frontend - * @see https://www.appsloveworld.com/vuejs/100/8/stream-shell-output-to-web-front-end - * @param data - */ -export default function ( - data: Ref> -): Function { - return function ( - _: Event, - result:string - ): void { - - data.value.output = result - } -} diff --git a/src/utils/onKruxVerifyOpenssl.ts b/src/utils/onKruxVerifyOpenssl.ts deleted file mode 100644 index a1c23d4d..00000000 --- a/src/utils/onKruxVerifyOpenssl.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Ref } from "vue"; -import messages from './messages' - -export default function (data: Ref>): Function { - return async function (_: Event, result: Record<'from' | 'message', any>): Promise { - if (result.from === 'KruxInstallerLogo') { - await messages.add(data, result.message) - await messages.close(data) - await window.api.invoke('krux:change:page', { page: 'Main', from: result.from }) - messages.clean(data) - } - } -} \ No newline at end of file diff --git a/src/utils/onKruxVerifyReleaseSign.ts b/src/utils/onKruxVerifyReleaseSign.ts deleted file mode 100644 index 55f927ba..00000000 --- a/src/utils/onKruxVerifyReleaseSign.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Ref } from "vue" - -/** - * Store result of signature verification - * (and its command) in `data`, then redirect to - * VerifiedOfficialRelease page - * @param data - * @returns Function - */ -export default function (data: Ref>): Function { - return async function onKruxVerifyReleaseSign (_: Event, result: Record<'command' | 'sign', any>): Promise { - data.value.command = result.command - data.value.sign = result.sign - await window.api.invoke('krux:change:page', { page: 'VerifiedOfficialRelease' }) - } -} \ No newline at end of file diff --git a/src/utils/onKruxVerifyReleasesFetch.ts b/src/utils/onKruxVerifyReleasesFetch.ts deleted file mode 100644 index 00d501f4..00000000 --- a/src/utils/onKruxVerifyReleasesFetch.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Ref } from "vue" - -export default function (data: Ref>): Function { - return async function (_event: Event, result: Record<'from' | 'key' | 'value', any>): Promise { - if (result.from === 'GithubChecker') { - data.value = { - versions: result.value - } - await window.api.invoke('krux:change:page', { page: 'SelectVersion' }) - } - } -} \ No newline at end of file diff --git a/src/utils/onKruxVerifyReleasesHash.ts b/src/utils/onKruxVerifyReleasesHash.ts deleted file mode 100644 index e56fd359..00000000 --- a/src/utils/onKruxVerifyReleasesHash.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Ref } from "vue" - -/** - * Store result of sha256sum verification in `data` - * and invoke signature verification - * - * @param data - * @returns Function - */ -export default function (data: Ref>): Function { - return async function (_: Event, result: Record<'name' | 'value', string>[]): Promise { - data.value.hash = result - await window.api.invoke('krux:verify:releases:sign') - } -} \ No newline at end of file diff --git a/src/utils/onKruxWipe.ts b/src/utils/onKruxWipe.ts deleted file mode 100644 index d67c94fd..00000000 --- a/src/utils/onKruxWipe.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Ref } from "vue"; - -export default function ( - data: Ref> -): Function { - return function ( - _: Event, - result:Record - ): void { - data.value.done = result.done - data.value.output = "" - } -} diff --git a/src/utils/onKruxWipeData.ts b/src/utils/onKruxWipeData.ts deleted file mode 100644 index 79df67a9..00000000 --- a/src/utils/onKruxWipeData.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Ref } from "vue"; - -// If you have no problems simply ignoring all type-checking features for this library, you have two options: -// Add @ts-ignore above all imports or Create a declaration file with any type, so all imports are automatically considered to be of any type. -// see https://stackoverflow.com/questions/56688893/how-to-use-a-module-when-it-could-not-find-a-declaration-file#answer-56690386 -// @ts-ignore -import { AnsiUp } from 'ansi_up' - -/** - * Stream shell output to web frontend - * @see https://www.appsloveworld.com/vuejs/100/8/stream-shell-output-to-web-front-end - * @param data - */ -export default function ( - data: Ref> -): Function { - return function ( - _: Event, - result:string - ): void { - const ansi = new AnsiUp() - let tmp = result.replace(/%\s/, "\n") - tmp = tmp.replace(/kiB\/s/g, "kiB/s\n") - - data.value.output = ansi.ansi_to_html(tmp).replace(/\n/gm, '
') - } -} diff --git a/src/utils/scanner/__init__.py b/src/utils/scanner/__init__.py new file mode 100644 index 00000000..a4024616 --- /dev/null +++ b/src/utils/scanner/__init__.py @@ -0,0 +1,26 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +__init__.py +""" + +from .cli_scanner import CliScanner diff --git a/src/utils/scanner/base_scanner.py b/src/utils/scanner/base_scanner.py new file mode 100644 index 00000000..f0c50e81 --- /dev/null +++ b/src/utils/scanner/base_scanner.py @@ -0,0 +1,61 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +base_scanner.py +""" + +import cv2 +from ..trigger import Trigger + + +class BaseScanner(Trigger): + """Base class for Scanner""" + + def __init__(self, capture_dev: int = 0): + super().__init__() + self.video_capture = cv2.VideoCapture(capture_dev) + + @property + def video_capture(self) -> cv2.VideoCapture: + """Getter for video capture""" + self.debug(f"video_capture::getter={self._video_capture}") + return self._video_capture + + @video_capture.setter + def video_capture(self, value: cv2.VideoCapture): + self.debug(f"video_capture::setter={value}") + self._video_capture = value + + def close_cli_capture(self): + """Close video capture for cli""" + self.video_capture.release() + cv2.destroyAllWindows() + + @staticmethod + def show_freeze_image(frame): + """Freeze a frame and show it""" + cv2.imgshow("frame", frame) + + @staticmethod + def on_click_quit(button: str = "q"): + """Capture if q quit button is pressed on keyboard""" + return cv2.waitKey(1) & ord(button) == 0xFF diff --git a/src/utils/scanner/cli_scanner.py b/src/utils/scanner/cli_scanner.py new file mode 100644 index 00000000..69a90579 --- /dev/null +++ b/src/utils/scanner/cli_scanner.py @@ -0,0 +1,53 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +cli_scanner.py +""" + +import typing +from pyzbar.pyzbar import decode +from .base_scanner import BaseScanner + + +class CliScanner(BaseScanner): + """Scanner for krux-installer as CLI""" + + def __init__(self): + super().__init__(capture_dev=0) + + def scan(self) -> typing.List: + """Open scan window and detect/decode a QRCode""" + + while True: + _ret, frame = self.video_capture.read() + qrcode = decode(frame) + + if qrcode: + break + + CliScanner.show_freeze_image(frame) + + if CliScanner.on_click_quit(): + break + + self.close_cli_capture() + return qrcode[0].data diff --git a/src/utils/selector/__init__.py b/src/utils/selector/__init__.py new file mode 100644 index 00000000..aeda328c --- /dev/null +++ b/src/utils/selector/__init__.py @@ -0,0 +1,149 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2023 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +selector.py + +Generic selector to select devices or versions +""" + +import typing +from http.client import HTTPResponse +import requests +from ..trigger import Trigger + +VALID_DEVICES = ( + "m5stickv", + "amigo", + "amigo_tft", + "amigo_ips", + "dock", + "bit", + "yahboom", + "cube", + "wonder_mv", +) + + +class Selector(Trigger): + """ + Class to select devices and firmware versions of krux + """ + + URL = "https://api.github.com/repos/selfcustody/krux/releases" + HEADERS = { + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + } + + def __init__(self): + super().__init__() + self.device = None + self.releases = self._fetch_releases() + self.firmware = None + + @property + def device(self) -> str: + """ + Get the current device + """ + self.debug(f"device::getter={self._device}") + return self._device + + @device.setter + def device(self, value: str): + """Setter for the current device""" + self.debug(f"device::setter={value}") + if value in VALID_DEVICES or value is None: + self.debug(f"device::setter={value}") + self._device = value + else: + raise ValueError(f"Device '{value}' is not valid") + + @property + def firmware(self) -> str: + """Getter for the current firmware version""" + self.debug(f"firmware::getter={self._firmware}") + return self._firmware + + @firmware.setter + def firmware(self, value: str): + """Setter for the current firmware version""" + if value in self.releases or value is None: + self.debug(f"firmware::setter={value}") + self._firmware = value + else: + raise ValueError(f"Firmware '{value}' is not valid") + + @property + def releases(self) -> typing.List[dict]: + """Getter of releases""" + self.debug(f"releases::getter={self._releases}") + return self._releases + + @releases.setter + def releases(self, value: typing.List[dict]): + """Set a list of releases""" + self.debug(f"releases::setter={value}") + self._releases = value + + def _fetch_releases(self, timeout: int = 10) -> HTTPResponse: + """ + Get the all available releases at + https://github.com/selfcustody/krux/releases + """ + try: + self.debug(f"releases::getter::URL={Selector.URL}") + accept = Selector.HEADERS["Accept"] + api = Selector.HEADERS["X-GitHub-Api-Version"] + self.debug(f"releases::getter::HEADER=Accept: {accept}") + self.debug(f"releases::getter::HEADER=X-Github-Api-Version: {api}") + response = requests.get( + url=Selector.URL, headers=Selector.HEADERS, timeout=timeout + ) + response.raise_for_status() + + except requests.exceptions.Timeout as t_exc: + raise RuntimeError(t_exc) from t_exc + + except requests.exceptions.ConnectionError as c_exc: + raise RuntimeError(c_exc) from c_exc + + except requests.exceptions.HTTPError as h_exc: + raise RuntimeError(h_exc) from h_exc + + res = response.json() + self.debug(f"releases::getter::response='{res}'") + + if len(res) == 0: + raise ValueError(f"{Selector.URL} returned empty data") + + obj = [] + for data in res: + if not data.get("tag_name"): + raise KeyError("Invalid key: 'tag_name' do not exist on api") + + obj.append(data["tag_name"]) + + self.debug(f"releases::getter={obj}") + self.debug("releases::getter::append=odudex/krux_binaries") + obj.append("odudex/krux_binaries") + return obj diff --git a/src/utils/setMainData.ts b/src/utils/setMainData.ts deleted file mode 100644 index 656f1d99..00000000 --- a/src/utils/setMainData.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Ref } from "vue" - -export default async function setMainData( - data: Ref>, - result: Record<'from' | 'key' | 'values', any> -): Promise { - data.value.device = result.values.device - data.value.version = result.values.version - data.value.os = result.values.os - data.value.isMac10 = result.values.isMac10 - data.value.showFlash = result.values.showFlash -} \ No newline at end of file diff --git a/src/utils/signer/__init__.py b/src/utils/signer/__init__.py new file mode 100644 index 00000000..4d85c05b --- /dev/null +++ b/src/utils/signer/__init__.py @@ -0,0 +1,25 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2023 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +__init__.py +""" +from .trigger_signer import TriggerSigner diff --git a/src/utils/signer/base_signer.py b/src/utils/signer/base_signer.py new file mode 100644 index 00000000..7a55b1c9 --- /dev/null +++ b/src/utils/signer/base_signer.py @@ -0,0 +1,140 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2023 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +base_signer.py +""" +import os +import re +import base64 +import typing +from ..trigger import Trigger + + +ASN1_STRUCTURE_FOR_PUBKEY = "3036301006072A8648CE3D020106052B8104000A032200" +""" +ASN.1 STRUCTURE FOR PUBKEY (uncompressed and compressed): + 30 <-- declares the start of an ASN.1 sequence + 56 <-- length of following sequence (dez 86) + 30 <-- length declaration is following + 10 <-- length of integer in bytes (dez 16) + 06 <-- declares the start of an "octet string" + 07 <-- length of integer in bytes (dez 7) + 2A 86 48 CE 3D 02 01 <-- Object Identifier: 1.2.840.10045.2.1 + = ecPublicKey, ANSI X9.62 public key type + 06 <-- declares the start of an "octet string" + 05 <-- length of integer in bytes (dez 5) + 2B 81 04 00 0A <-- Object Identifier: 1.3.132.0.10 + = secp256k1, SECG (Certicom) named eliptic curve + 03 <-- declares the start of an "octet string" + 42 <-- length of bit string to follow (66 bytes) + 00 <-- Start pubkey?? +""" + + +class BaseSigner(Trigger): + """ + BaseSigner + """ + + def __init__(self, filename: str): + super().__init__() + self.filename = filename + + @property + def filename(self) -> str: + """Getter for filename""" + self.debug(f"filename::getter={self._filename}") + return self._filename + + @filename.setter + def filename(self, value: str): + """Setter for filename""" + if os.path.exists(value): + self.debug(f"filename::setter={value}") + self._filename = value + else: + raise ValueError(f"{value} do not exists") + + @property + def filehash(self) -> str: + """Getter for filehash""" + try: + self.debug(f"filehash::getter={self._filehash}") + return self._filehash + except AttributeError: + return None + + @filehash.setter + def filehash(self, value: str): + """Setter for filehash""" + if re.findall(r"[a-fA-F0-9]{64}", value): + self.debug(f"filehash::setter={value}") + self._filehash = value + else: + raise ValueError(f"Invalid hash: {value}") + + @property + def signature(self) -> typing.SupportsBytes: + """Getter for signature in byte format""" + try: + self.debug(f"signature::getter={self._signature}") + return self._signature + except AttributeError: + return None + + @signature.setter + def signature(self, value: str): + """Setter for signature giving a well formated string""" + if re.findall( + r"^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", value + ): + self.debug(f"signature::setter={value}") + self._signature = base64.b64decode(value.encode()) + else: + raise ValueError(f"Invalid signature: {value}") + + @property + def pubkey(self) -> str: + """Getter for public key certificate""" + try: + self.debug(f"pubkey::getter={self._pubkey}") + return self._pubkey + except AttributeError: + return None + + @pubkey.setter + def pubkey(self, value: typing.SupportsBytes): + """Setter for public key certificate""" + if re.findall("[a-f0-9]{64}", value): + self.debug(f"pubkey::setter={value}") + pubkey_data = f"{ASN1_STRUCTURE_FOR_PUBKEY}{value}" + + # Convert pubkey data to bytes + pubkey_data_bytes = bytes.fromhex(pubkey_data) + + # Encoding bytes to base64 format + pubkey_data_b64 = base64.b64encode(pubkey_data_bytes) + + # Decode base64 to utf8 + self._pubkey = pubkey_data_b64.decode("utf8") + else: + raise ValueError(f"Invalid pubkey: {value}") diff --git a/src/utils/signer/trigger_signer.py b/src/utils/signer/trigger_signer.py new file mode 100644 index 00000000..326948f5 --- /dev/null +++ b/src/utils/signer/trigger_signer.py @@ -0,0 +1,81 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2023 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +signer.py +""" +import hashlib +from .base_signer import BaseSigner + + +class TriggerSigner(BaseSigner): + """ + Signer is the class that manages the `sign` command. + """ + + def __init__(self, filename: str): + super().__init__(filename=filename) + + def make_hash(self): + """Create a file hash before sign""" + with open(self.filename, "rb") as f_sig: + _bytes = f_sig.read() + data = hashlib.sha256(_bytes).hexdigest() + self.filehash = data + + def save_hash(self): + """Save file's hash in a sha256.txt file""" + if self.filehash is None: + raise ValueError("Empty hash") + + filehashname = f"{self.filename}.sha256.txt" + with open(filehashname, mode="w", encoding="utf-8") as h_file: + content = f"{self.filehash} {self.filename}" + h_file.write(content) + self.debug(f"{filehashname} saved") + + def save_signature(self): + """Save the signature data into a .sig file""" + if not self.signature is None: + sigfile = f"{self.filename}.sig" + with open(sigfile, "wb") as s_file: + s_file.write(self.signature) + self.debug(f"{sigfile} saved") + else: + raise ValueError("Empty signature") + + def save_pubkey(self): + """Create PEM data""" + if not self.pubkey is None: + # Format pubkey + formated_pubkey = "\n".join( + [ + "-----BEGIN PUBLIC KEY-----", + self.pubkey, + "-----END PUBLIC KEY-----", + ] + ) + pubfile = f"{self.filename}.pem" + with open(pubfile, mode="w", encoding="utf-8") as pb_file: + pb_file.write(formated_pubkey) + self.debug(f"{pubfile} saved") + else: + raise ValueError("Empty pubkey") diff --git a/src/utils/trigger/__init__.py b/src/utils/trigger/__init__.py new file mode 100644 index 00000000..e225cec2 --- /dev/null +++ b/src/utils/trigger/__init__.py @@ -0,0 +1,58 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2023 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +trigger.py + +Base class to be used accross project +""" +import os +from kivy.logger import Logger +from src.utils.info import mro + + +class Trigger: + """ + Trigger + + Class to be (co)inherited in any class of the project. + All actions will be logged. + """ + + def info(self, msg: str): + """Logger with level 'info'""" + Logger.info("%s: %s", mro(), msg) + + def debug(self, msg: str): + """Logger with level 'debug'""" + Logger.debug("%s: %s", mro(), msg) + + def warning(self, msg: str): + """Logger with level 'warning'""" + Logger.warning("%s: %s", mro(), msg) + + def error(self, msg: str): + """Logger with level 'critical'""" + Logger.error("%s: %s", mro(), msg) + + def critical(self, msg: str): + """Logger with level 'critical'""" + Logger.critical("%s: %s", mro(), msg) diff --git a/src/utils/unzip/__init__.py b/src/utils/unzip/__init__.py new file mode 100644 index 00000000..710c530b --- /dev/null +++ b/src/utils/unzip/__init__.py @@ -0,0 +1,6 @@ +""" +__init__.py +""" + +from .firmware_unzip import FirmwareUnzip +from .kboot_unzip import KbootUnzip diff --git a/src/utils/unzip/base_unzip.py b/src/utils/unzip/base_unzip.py new file mode 100644 index 00000000..6f4fefd0 --- /dev/null +++ b/src/utils/unzip/base_unzip.py @@ -0,0 +1,98 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +base_unzip.py +""" +import os +import tempfile +import typing +from zipfile import ZipFile, BadZipFile +from ..verifyer.check_verifyer import CheckVerifyer + + +class BaseUnzip(CheckVerifyer): + """Base class to unzip files""" + + def __init__( + self, + filename: str, + members: typing.List[str], + output: str = tempfile.gettempdir(), + ): + super().__init__(filename=filename, read_mode="r", regexp=r".*\.zip") + + # make an unordered list of members + self.members = members + self.output = output + + @property + def members(self) -> typing.List[str]: + """Getter for the name of members files to be extracted from zip""" + self.debug(f"members::getter={self._filename}") + return list(self._members) + + @members.setter + def members(self, value: typing.List[str]): + """Setter for the name of file to be extracted""" + if len(value) > 0: + self.debug(f"members::setter={value}") + self._members = set(value) + else: + raise ValueError("Members cannot be empty") + + @property + def output(self) -> str: + """Getter for the path where extracted files will be placed""" + self.debug(f"output::getter={self._output}") + return self._output + + @output.setter + def output(self, value: str): + """Setter for the path where extracted files will be placed""" + if os.path.exists(value): + self.debug(f"output::setter={value}") + self._output = value + else: + raise ValueError(f"Given path not exist: {value}") + + @staticmethod + def sanitized_base_name(filename): + """Extract the name of zip release without folder tree and .zip extension""" + base_name = os.path.basename(filename) + return base_name.replace(".zip", "") + + def load(self): + """Extract from given zip file only the ones that was defined as members""" + try: + self.debug(f"load::opening={self.filename}") + with ZipFile(self.filename, self.read_mode) as zip_obj: + namelist = set(zip_obj.namelist()) + self.debug(f"load::namelist={namelist}") + for name in namelist: + if name in self.members: + self.debug(f"load::extract::{self.filename}={name}") + zip_obj.extract(name, path=self.output) + + except BadZipFile as exc_info: + raise RuntimeError( + f"Cannot open {self.filename}: {exc_info.__cause__}" + ) from exc_info diff --git a/src/utils/unzip/firmware_unzip.py b/src/utils/unzip/firmware_unzip.py new file mode 100644 index 00000000..4237bbfd --- /dev/null +++ b/src/utils/unzip/firmware_unzip.py @@ -0,0 +1,41 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +firmware_unzip.py +""" +import tempfile +from .base_unzip import BaseUnzip + + +class FirmwareUnzip(BaseUnzip): + """Unzip packaged firmware""" + + def __init__(self, filename: str, device: str, output: str = tempfile.gettempdir()): + base_name = BaseUnzip.sanitized_base_name(filename) + super().__init__( + filename=filename, + members=[ + f"{base_name}/maixpy_{device}/firmware.bin", + f"{base_name}/maixpy_{device}/firmware.bin.sig", + ], + output=output, + ) diff --git a/src/utils/unzip/kboot_unzip.py b/src/utils/unzip/kboot_unzip.py new file mode 100644 index 00000000..e523026b --- /dev/null +++ b/src/utils/unzip/kboot_unzip.py @@ -0,0 +1,38 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +kboot_unzip.py +""" +import tempfile +from .base_unzip import BaseUnzip + + +class KbootUnzip(BaseUnzip): + """Unzip packaged firmware""" + + def __init__(self, filename: str, device: str, output: str = tempfile.gettempdir()): + base_name = BaseUnzip.sanitized_base_name(filename) + super().__init__( + filename=filename, + members=[f"{base_name}/maixpy_{device}/kboot.kfpkg"], + output=output, + ) diff --git a/src/utils/verifyer/__init__.py b/src/utils/verifyer/__init__.py new file mode 100644 index 00000000..d232d788 --- /dev/null +++ b/src/utils/verifyer/__init__.py @@ -0,0 +1,30 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +__init__.py +""" +# pylint: disable=unused-import +from .sha256_check_verifyer import Sha256CheckVerifyer +from .sig_check_verifyer import SigCheckVerifyer +from .pem_check_verifyer import PemCheckVerifyer +from .sha256_verifyer import Sha256Verifyer +from .sig_verifyer import SigVerifyer diff --git a/src/utils/verifyer/base_verifyer.py b/src/utils/verifyer/base_verifyer.py new file mode 100644 index 00000000..082297b3 --- /dev/null +++ b/src/utils/verifyer/base_verifyer.py @@ -0,0 +1,76 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +base_verifyer.py +""" + +import typing +from ..trigger import Trigger + + +class BaseVerifyer(Trigger): + """Base class for verifyers""" + + def __init__(self, filename: str, read_mode: str): + super().__init__() + self.filename = filename + self.read_mode = read_mode + self.data = None + + @property + def filename(self) -> str: + """Getter for filename""" + self.debug(f"filename::getter={self._filename}") + return self._filename + + @filename.setter + def filename(self, value: str): + """Setter for filename""" + self.debug(f"filename::setter={value}") + self._filename = value + + @property + def read_mode(self) -> str: + """Getter for read_mode (r or rb)""" + self.debug(f"read_mode::getter={self._read_mode}") + return self._read_mode + + @read_mode.setter + def read_mode(self, value: str): + """Setter for read_mode""" + if value in ("r", "rb"): + self.debug(f"read_mode::setter={value}") + self._read_mode = value + else: + raise ValueError(f"Invalid read_mode: {value}") + + @property + def data(self) -> str | typing.SupportsBytes: + """Getter for loaded data""" + self.debug(f"data::getter={self._data}") + return self._data + + @data.setter + def data(self, value: str | typing.SupportsBytes): + """Setter for data""" + self.debug(f"data::setter={value}") + self._data = value diff --git a/src/utils/verifyer/check_verifyer.py b/src/utils/verifyer/check_verifyer.py new file mode 100644 index 00000000..f35c6948 --- /dev/null +++ b/src/utils/verifyer/check_verifyer.py @@ -0,0 +1,78 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +check_verifyer.py +""" + +import os +import re +import typing +from .base_verifyer import BaseVerifyer + + +class CheckVerifyer(BaseVerifyer): + """basic class for *CheckVerifyer class (do not use directly)""" + + def __init__(self, filename: str, read_mode: str, regexp: typing.re): + if not re.findall(regexp, filename): + raise ValueError(f"Invalid file: {filename} do not assert with {regexp}") + + if not os.path.exists(filename): + raise ValueError(f"File {filename} do not exist") + + super().__init__(filename=filename, read_mode=read_mode) + + def load(self): + """Load data in file""" + self.debug(f"load::{self.filename}::{self.read_mode}") + if self.read_mode == "r": + with open(self.filename, self.read_mode, encoding="utf8") as f_data: + self.data = f_data.read().strip() + + if self.read_mode == "rb": + # pylint: disable=unspecified-encoding + with open(self.filename, self.read_mode) as f_data: + self.data = f_data.read() + + @property + def certificate(self) -> typing.SupportsBytes: + """Getter for certificate""" + self.debug(f"certificate::getter={self._certificate}") + return self._certificate + + @certificate.setter + def certificate(self, value: typing.SupportsBytes): + """Setter for certificate""" + self.debug(f"certificate::setter={value}") + self._certificate = value + + @property + def signature(self) -> typing.SupportsBytes: + """Getter for signature bytes""" + self.debug(f"signature::getter={self._signature}") + return self._signature + + @signature.setter + def signature(self, value: typing.SupportsBytes): + """Set the public key on X509 object""" + self.debug(f"signature::setter={value}") + self._signature = value diff --git a/src/utils/verifyer/pem_check_verifyer.py b/src/utils/verifyer/pem_check_verifyer.py new file mode 100644 index 00000000..ee608170 --- /dev/null +++ b/src/utils/verifyer/pem_check_verifyer.py @@ -0,0 +1,33 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +pem_check_verifyer.py +""" + +from .check_verifyer import CheckVerifyer + + +class PemCheckVerifyer(CheckVerifyer): + """The checker verifyer for .pem files""" + + def __init__(self, filename: str): + super().__init__(filename=filename, read_mode="rb", regexp=r".*\.pem") diff --git a/src/utils/verifyer/sha256_check_verifyer.py b/src/utils/verifyer/sha256_check_verifyer.py new file mode 100644 index 00000000..9ffc1fbf --- /dev/null +++ b/src/utils/verifyer/sha256_check_verifyer.py @@ -0,0 +1,33 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +sha256_check_verifyer.py +""" + +from .check_verifyer import CheckVerifyer + + +class Sha256CheckVerifyer(CheckVerifyer): + """Simple loader of a .sha256.txt file""" + + def __init__(self, filename: str): + super().__init__(filename=filename, read_mode="r", regexp=r".*\.sha256\.txt") diff --git a/src/utils/verifyer/sha256_verifyer.py b/src/utils/verifyer/sha256_verifyer.py new file mode 100644 index 00000000..2afbedc1 --- /dev/null +++ b/src/utils/verifyer/sha256_verifyer.py @@ -0,0 +1,57 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +sha256_verifyer.py +""" + +import os +import hashlib +from .base_verifyer import BaseVerifyer + + +class Sha256Verifyer(BaseVerifyer): + """Verify sh256 checksum against a provided .sha256.txt file""" + + def __init__(self, filename: str): + if os.path.exists(filename): + super().__init__(filename, "rb") + else: + raise ValueError(f"File {filename} do not exist") + + def load(self): + """Load data from file and assigns its sha256sum""" + sha256_hash = hashlib.sha256() + + self.debug(f"load::{self.filename}::{self.read_mode}") + + # pylint: disable=unspecified-encoding + with open(self.filename, self.read_mode) as f_data: + # Read and update hash string value in blocks of 1K bytes + for byte_block in iter(lambda: f_data.read(1024), b""): + self.debug(f"load::block={byte_block}") + sha256_hash.update(byte_block) + + self.data = sha256_hash.hexdigest() + + def verify(self, sha256sum: str) -> bool: + """Verify self.hash against a providede sha256_hash""" + return self.data == sha256sum diff --git a/src/utils/verifyer/sig_check_verifyer.py b/src/utils/verifyer/sig_check_verifyer.py new file mode 100644 index 00000000..f9596c32 --- /dev/null +++ b/src/utils/verifyer/sig_check_verifyer.py @@ -0,0 +1,33 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +sig_check_verifyer.py +""" + +from .check_verifyer import CheckVerifyer + + +class SigCheckVerifyer(CheckVerifyer): + """The checker verifyer for .sig files""" + + def __init__(self, filename: str): + super().__init__(filename=filename, read_mode="rb", regexp=r".*\.sig") diff --git a/src/utils/verifyer/sig_verifyer.py b/src/utils/verifyer/sig_verifyer.py new file mode 100644 index 00000000..8253b3ec --- /dev/null +++ b/src/utils/verifyer/sig_verifyer.py @@ -0,0 +1,48 @@ +# The MIT License (MIT) + +# Copyright (c) 2021-2024 Krux contributors + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +sig_verifyer.py +""" + +import typing +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives import serialization, hashes, asymmetric +from .check_verifyer import CheckVerifyer + + +class SigVerifyer(CheckVerifyer): + """Verify file signature agains .sig and .pem data""" + + def __init__(self, filename: str, signature: str, pubkey: str, regexp: typing.re): + super().__init__(filename=filename, read_mode="rb", regexp=regexp) + self.certificate = serialization.load_pem_public_key(pubkey) + self.signature = signature + + def verify(self) -> bool: + """Apply signature verification against a signature data and public key data""" + try: + algorithm = asymmetric.ec.ECDSA(hashes.SHA256()) + self.certificate.verify(self.signature, self.data, algorithm) + return True + except InvalidSignature as exc_info: + print(exc_info) + return False diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts deleted file mode 100644 index 323c78a6..00000000 --- a/src/vite-env.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -/// - -declare module '*.vue' { - import type { DefineComponent } from 'vue' - const component: DefineComponent<{}, {}, any> - export default component -} diff --git a/test/e2e/pageobjects/app.page.ts b/test/e2e/pageobjects/app.page.ts deleted file mode 100644 index 9e355700..00000000 --- a/test/e2e/pageobjects/app.page.ts +++ /dev/null @@ -1,590 +0,0 @@ -const { $ } = require('@wdio/globals') - -class App { - - private __app__: string; - private __main__: string; - private __logo__: string; - private __loading_data_msg__: string; - private __verifying_openssl_msg__: string; - - private __openssl_for_linux_found__: string; - private __openssl_for_darwin_found__: string; - private __openssl_for_win32_found__: string; - - private __main_page__: string; - private __main_page_click_message_text__: string; - private __main_page_select_device_button__: string; - private __main_page_select_version_button__: string; - private __main_page_select_device_text__: string; - private __main_page_select_version_text__: string; - private __main_page_flash_button__: string; - private __main_page_flash_text__: string; - - private __select_device_page__: string; - private __select_device_page_maixpy_m5stickv_button__: string; - private __select_device_page_maixpy_amigo_button__: string; - private __select_device_page_maixpy_bit_button__: string; - private __select_device_page_maixpy_dock_button__: string; - private __select_device_page_maixpy_back_button__: string; - private __select_device_page_maixpy_m5stickv_text__: string; - private __select_device_page_maixpy_amigo_text__: string; - private __select_device_page_maixpy_bit_text__: string; - private __select_device_page_maixpy_dock_text__: string; - private __select_device_page_maixpy_back_text__: string; - - private __select_version_page__: string; - private __select_version_page_selfcustody_button__: string; - private __select_version_page_selfcustody_text__: string; - private __select_version_page_odudex_button__: string; - private __select_version_page_odudex_text__: string; - private __select_version_page_back_button__: string; - private __select_version_page_back_text__: string; - - private __github_octocat_checker_logo__: string; - - private __download_official_release_zip_page__: string; - private __download_official_release_zip_title__: string; - private __download_official_release_zip_subtitle__: string; - private __download_official_release_zip_progress__: string; - - private __checking_release_zip_msg__: string; - private __not_found_release_zip_msg__: string; - private __found_release_zip_msg__: string; - - private __checking_release_zip_sha256_txt_msg__: string; - private __not_found_release_zip_sha256_txt_msg__: string; - private __found_release_zip_sha256_txt_msg__: string; - - private __download_official_release_zip_sha256_txt_page__: string; - private __download_official_release_zip_sha256_txt_page_title__: string; - private __download_official_release_zip_sha256_txt_page_subtitle__: string; - private __download_official_release_zip_sha256_txt_page_progress__: string; - - private __checking_release_zip_sig_msg__: string; - private __not_found_release_zip_sig_msg__: string; - private __found_release_zip_sig_msg__: string; - - private __download_official_release_zip_sig_page__: string; - private __download_official_release_zip_sig_title__: string; - private __download_official_release_zip_sig_subtitle__: string; - private __download_official_release_zip_sig_progress__: string; - - private __checking_release_pem_msg__: string; - private __not_found_release_pem_msg__: string; - private __found_release_pem_msg__: string; - - private __download_official_release_pem_page__: string; - private __download_official_release_pem_title__: string; - private __download_official_release_pem_subtitle__: string; - private __download_official_release_pem_progress__: string; - - private __warning_download_page__: string; - private __warning_already_downloaded_text__ : string; - private __warning_download_proceed_button__: string; - private __warning_download_proceed_button_text__: string; - private __warning_download_show_details_button__: string; - private __warning_download_show_details_button_text__: string; - private __warning_download_again_button__: string - private __warning_download_again_button_text__: string; - private __warning_download_back_button__: string; - private __warning_download_back_button_text__: string; - private __warning_already_downloaded_overlay__: string; - private __warning_already_downloaded_overlay_title__: string; - private __warning_already_downloaded_overlay_subtitle__: string; - private __warning_already_downloaded_overlay_text_remote__: string; - private __warning_already_downloaded_overlay_text_local__: string; - private __warning_already_downloaded_overlay_text_whatdo__: string; - private __warning_already_downloaded_overlay_button_close__: string - - private __check_verify_official_release_page__: string; - private __check_verify_official_release_len_sassaman_is_using_openssl__: string; - - private __verified_official_release_page__: string - private __verified_official_release_page_sha256_integrity_title__: string; - - private __verified_official_release_page_sha256_integrity_txt__: string; - private __verified_official_release_page_sha256_integrity__: string; - private __verified_official_release_page_signature_title__: string; - private __verified_official_release_page_signature_command__: string; - private __verified_official_release_page_signature_result__: string; - private __verified_official_release_page_back_button__: string; - - - constructor () { - this.__app__ = '#app' - this.__main__ = '#app>div>main' - this.__logo__ = "pre#krux-installer-logo" - this.__loading_data_msg__ = 'pre#loading-data-from-storage' - this.__verifying_openssl_msg__ = 'pre#verifying-openssl' - - this.__openssl_for_linux_found__ = 'pre#openssl-for-linux-found' - this.__openssl_for_darwin_found__ = 'pre#openssl-for-darwin-found' - this.__openssl_for_win32_found__ = 'pre#openssl-for-win32-found' - - this.__main_page__ = '#main-page' - this.__main_page_click_message_text__ = 'div#main-click-message-text' - this.__main_page_select_device_button__ = 'div#main-page-select-device-button' - this.__main_page_select_version_button__ = 'div#main-page-select-version-button' - this.__main_page_select_device_text__ = 'div#main-page-select-device-text' - this.__main_page_select_version_text__ = 'div#main-page-select-version-text' - this.__main_page_flash_button__ = 'div#main-page-flash-button' - this.__main_page_flash_text__ = 'div#main-page-flash-text' - - this.__select_device_page__ = 'div#select-device-page'; - this.__select_device_page_maixpy_m5stickv_button__ = 'div#select-device-page-maixpy_m5stickv-button' - this.__select_device_page_maixpy_amigo_button__ = 'div#select-device-page-maixpy_amigo-button' - this.__select_device_page_maixpy_bit_button__ = 'div#select-device-page-maixpy_bit-button' - this.__select_device_page_maixpy_dock_button__ = 'div#select-device-page-maixpy_dock-button' - this.__select_device_page_maixpy_back_button__ = 'div#select-device-page-back-button' - this.__select_device_page_maixpy_m5stickv_text__ = 'div#select-device-page-maixpy_m5stickv-text' - this.__select_device_page_maixpy_amigo_text__ = 'div#select-device-page-maixpy_amigo-text' - this.__select_device_page_maixpy_bit_text__ = 'div#select-device-page-maixpy_bit-text' - this.__select_device_page_maixpy_dock_text__ = 'div#select-device-page-maixpy_dock-text' - this.__select_device_page_maixpy_back_text__ = 'div#select-device-page-back-text' - - this.__select_version_page__ = 'div#select-version-page'; - this.__select_version_page_selfcustody_button__ = 'div#select-version-page-selfcustody-krux-releases-tag-v24-03-0-button' - this.__select_version_page_back_button__ = 'div#select-version-page-back-button' - this.__select_version_page_odudex_button__ = 'div#select-version-page-odudex-krux-binaries-button' - this.__select_version_page_selfcustody_text__ = 'div#select-version-page-selfcustody-krux-releases-tag-v24-03-0-text' - this.__select_version_page_odudex_text__ = 'div#select-version-page-odudex-krux-binaries-text' - this.__select_version_page_back_text__ = 'div#select-version-page-back-text' - - this.__github_octocat_checker_logo__ = 'pre#github-octocat-checker-logo' - - this.__download_official_release_zip_page__ = 'div#download-official-release-zip-page' - this.__download_official_release_zip_title__ = 'div#download-official-release-zip-page-title' - this.__download_official_release_zip_subtitle__ = 'div#download-official-release-zip-page-subtitle' - this.__download_official_release_zip_progress__ = 'div#download-official-release-zip-page-progress' - - this.__checking_release_zip_msg__ = 'pre#checking-v24-03-0-krux-v24-03-0-zip' - this.__not_found_release_zip_msg__ = 'pre#v24-03-0-krux-v24-03-0-zip-not-found' - this.__found_release_zip_msg__ = 'pre#v24-03-0-krux-v24-03-0-zip-found' - - this.__checking_release_zip_sha256_txt_msg__ = 'pre#checking-v24-03-0-krux-v24-03-0-zip-sha256-txt' - this.__not_found_release_zip_sha256_txt_msg__ = 'pre#v24-03-0-krux-v24-03-0-zip-sha256-txt-not-found' - this.__found_release_zip_sha256_txt_msg__ = 'pre#v24-03-0-krux-v24-03-0-zip-sha256-txt-found' - - this.__download_official_release_zip_sha256_txt_page__ = 'div#download-official-release-zip-sha256-txt-page' - this.__download_official_release_zip_sha256_txt_page_title__ = 'div#download-official-release-zip-sha256-txt-page-title' - this.__download_official_release_zip_sha256_txt_page_subtitle__ = 'div#download-official-release-zip-sha256-txt-page-subtitle' - this.__download_official_release_zip_sha256_txt_page_progress__ = 'div#download-official-release-zip-sha256-txt-page-progress' - - this.__checking_release_zip_sig_msg__ = 'pre#checking-v24-03-0-krux-v24-03-0-zip-sig' - this.__not_found_release_zip_sig_msg__ = 'pre#v24-03-0-krux-v24-03-0-zip-sig-not-found' - this.__found_release_zip_sig_msg__ = 'pre#v24-03-0-krux-v24-03-0-zip-sig-found' - - this.__download_official_release_zip_sig_page__ = 'div#download-official-release-zip-sig-page' - this.__download_official_release_zip_sig_title__ = 'div#download-official-release-zip-sig-page-title' - this.__download_official_release_zip_sig_subtitle__ = 'div#download-official-release-zip-sig-page-subtitle' - this.__download_official_release_zip_sig_progress__ = 'div#download-official-release-zip-sig-page-progress' - - this.__download_official_release_pem_page__ = 'div#download-official-release-pem-page' - this.__download_official_release_pem_title__ = 'div#download-official-release-pem-page-title' - this.__download_official_release_pem_subtitle__ = 'div#download-official-release-pem-page-subtitle' - this.__download_official_release_pem_progress__ = 'div#download-official-release-pem-page-progress' - - this.__checking_release_pem_msg__ = 'pre#checking-main-selfcustody-pem' - this.__not_found_release_pem_msg__ = 'pre#main-selfcustody-pem-not-found' - this.__found_release_pem_msg__ = 'pre#main-selfcustody-pem-found' - - this.__warning_download_page__ = 'div#warning-download-page' - this.__warning_already_downloaded_text__ = 'div#warning-already-downloaded-text' - this.__warning_download_proceed_button__ = 'div#warning-download-proceed-button' - this.__warning_download_proceed_button_text__ = 'div#warning-download-proceed-button-text' - this.__warning_download_again_button__ = 'div#warning-download-again-button' - this.__warning_download_again_button_text__ = 'div#warning-download-again-button-text' - this.__warning_download_show_details_button__ = 'div#warning-download-show-details-button' - this.__warning_download_show_details_button_text__ = 'div#warning-download-show-details-button-text' - this.__warning_download_back_button__ = 'div#warning-download-back-button' - this.__warning_download_back_button_text__ = 'div#warning-download-back-button-text' - this.__warning_already_downloaded_overlay__ = 'div#warning-already-downloaded-overlay' - this.__warning_already_downloaded_overlay_title__ = 'div#warning-already-downloaded-overlay-title' - this.__warning_already_downloaded_overlay_subtitle__ = 'div#warning-already-downloaded-overlay-subtitle' - this.__warning_already_downloaded_overlay_text_remote__ = 'div#warning-already-downloaded-overlay-text-remote' - this.__warning_already_downloaded_overlay_text_local__ = 'div#warning-already-downloaded-overlay-text-local' - this.__warning_already_downloaded_overlay_text_whatdo__ = 'div#warning-already-downloaded-overlay-text-whatdo' - this.__warning_already_downloaded_overlay_button_close__ = 'div#warning-already-downloaded-overlay-button-close' - - this.__check_verify_official_release_page__ = 'div#check-verify-official-release-page' - this.__check_verify_official_release_len_sassaman_is_using_openssl__ = 'pre#check-verify-official-release-len-sassaman-is-using-openssl' - - this.__verified_official_release_page__ = 'div#verified-official-release-page' - this.__verified_official_release_page_sha256_integrity_title__ = 'div#verified-official-release-page-sha256-integrity-title' - this.__verified_official_release_page_sha256_integrity_txt__ = 'div#verified-official-release-page-sha256-integrity-v24-03-0-krux-v24-03-0-zip-sha256-txt' - this.__verified_official_release_page_sha256_integrity__ = 'div#verified-official-release-page-sha256-integrity-v24-03-0-krux-v24-03-0-zip' - this.__verified_official_release_page_signature_title__ = 'div#verified-official-release-page-signature-title' - this.__verified_official_release_page_signature_command__ = 'span#verified-official-release-page-signature-command' - this.__verified_official_release_page_signature_result__ = 'span#verified-official-release-page-signature-result' - this.__verified_official_release_page_back_button__ = 'div#verified-official-release-page-back-button' - } - - get title () { - return $('head>title') - } - - get app () { - return $(this.__app__) - } - - get main () { - return $(this.__main__) - } - - get logo () { - return $(this.__logo__) - } - - get loadingDataMsg () { - return $(this.__loading_data_msg__) - } - - get verifyingOpensslMsg () { - return $(this.__verifying_openssl_msg__) - } - - get opensslForLinuxFound () { - return $(this.__openssl_for_linux_found__) - } - - get opensslForDarwinFound () { - return $(this.__openssl_for_darwin_found__) - } - - get opensslForWin32Found () { - return $(this.__openssl_for_win32_found__) - } - - get mainPage () { - return $(this.__main_page__) - } - - get mainClickMessageText () { - return $(this.__main_page_click_message_text__) - } - - get mainSelectDeviceButton () { - return $(this.__main_page_select_device_button__) - } - - get mainSelectVersionButton () { - return $(this.__main_page_select_version_button__) - } - - get mainSelectFlashButton () { - return $(this.__main_page_flash_button__) - } - - get mainSelectDeviceText () { - return $(this.__main_page_select_device_text__) - } - - get mainSelectVersionText () { - return $(this.__main_page_select_version_text__) - } - - get mainSelectFlashText () { - return $(this.__main_page_flash_text__) - } - - get selectDevicePage () { - return $(this.__select_device_page__) - } - - get selectMaixpyM5StickVButton () { - return $(this.__select_device_page_maixpy_m5stickv_button__) - } - - get selectMaixpyAmigoButton () { - return $(this.__select_device_page_maixpy_amigo_button__) - } - - get selectMaixpyBitButton () { - return $(this.__select_device_page_maixpy_bit_button__) - } - - get selectMaixpyDockButton () { - return $(this.__select_device_page_maixpy_dock_button__) - } - - get selectBackButton () { - return $(this.__select_device_page_maixpy_back_button__) - } - - get selectMaixpyM5StickVText () { - return $(this.__select_device_page_maixpy_m5stickv_text__) - } - - get selectMaixpyAmigoText () { - return $(this.__select_device_page_maixpy_amigo_text__) - } - - get selectMaixpyBitText () { - return $(this.__select_device_page_maixpy_bit_text__) - } - - get selectMaixpyDockText () { - return $(this.__select_device_page_maixpy_dock_text__) - } - - get selectBackText () { - return $(this.__select_device_page_maixpy_back_text__) - } - - get selectVersionPage () { - return $(this.__select_version_page__) - } - - get selectVersionSelfcustodyButton () { - return $(this.__select_version_page_selfcustody_button__) - } - - get selectVersionOdudexButton () { - return $(this.__select_version_page_odudex_button__) - } - - get selectVersionBackButton () { - return $(this.__select_version_page_back_button__) - } - - get selectVersionSelfcustodyText () { - return $(this.__select_version_page_selfcustody_text__) - } - - get selectVersionOdudexText () { - return $(this.__select_version_page_odudex_text__) - } - - get selectVersionBackText () { - return $(this.__select_version_page_back_text__) - } - - get githubOctocatCheckerLogo () { - return $(this.__github_octocat_checker_logo__) - } - - - get checkingReleaseZipMsg () { - return $(this.__checking_release_zip_msg__) - } - - get notFoundReleaseZipMsg () { - return $(this.__not_found_release_zip_msg__) - } - - get foundReleaseZipMsg () { - return $(this.__found_release_zip_msg__) - } - - get downloadOfficialReleaseZipPage () { - return $(this.__download_official_release_zip_page__) - } - - get downloadOfficialReleaseZipTitle () { - return $(this.__download_official_release_zip_title__) - } - - get downloadOfficialReleaseZipSubtitle () { - return $(this.__download_official_release_zip_subtitle__) - } - - get downloadOfficialReleaseZipProgress () { - return $(this.__download_official_release_zip_progress__) - } - - get warningDownloadPage () { - return $(this.__warning_download_page__) - } - - get warningAlreadyDownloadedText() { - return $(this.__warning_already_downloaded_text__) - } - - get warningDownloadProceedButton () { - return $(this.__warning_download_proceed_button__) - } - - get warningDownloadProceedButtonText () { - return $(this.__warning_download_proceed_button_text__) - } - - get warningDownloadAgainButton () { - return $(this.__warning_download_again_button__) - } - - get warningDownloadAgainButtonText () { - return $(this.__warning_download_again_button_text__) - } - - get warningDownloadShowDetailsButton () { - return $(this.__warning_download_show_details_button__) - } - - get warningDownloadShowDetailsButtonText () { - return $(this.__warning_download_show_details_button_text__) - } - - get warningDownloadBackButton () { - return $(this.__warning_download_back_button__) - } - - get warningDownloadBackButtonText () { - return $(this.__warning_download_back_button_text__) - } - - get warningAlreadyDownloadedOverlay () { - return $(this.__warning_already_downloaded_overlay__) - } - - get warningAlreadyDownloadedOverlayTitle () { - return $(this.__warning_already_downloaded_overlay_title__) - } - - get warningAlreadyDownloadedOverlaySubtitle () { - return $(this.__warning_already_downloaded_overlay_subtitle__) - } - - get warningAlreadyDownloadedOverlayTextRemote () { - return $(this.__warning_already_downloaded_overlay_text_remote__) - } - - get warningAlreadyDownloadedOverlayTextLocal () { - return $(this.__warning_already_downloaded_overlay_text_local__) - } - - get warningAlreadyDownloadedOverlayTextWhatdo () { - return $(this.__warning_already_downloaded_overlay_text_whatdo__) - } - - get warningAlreadyDownloadedOverlayButtonClose () { - return $(this.__warning_already_downloaded_overlay_button_close__) - } - - get checkingReleaseZipSha256txtMsg () { - return $(this.__checking_release_zip_sha256_txt_msg__) - } - - get notFoundReleaseZipSha256txtMsg () { - return $(this.__not_found_release_zip_sha256_txt_msg__) - } - - get foundReleaseZipSha256txtMsg () { - return $(this.__found_release_zip_sha256_txt_msg__) - } - - get downloadOfficialReleaseZipSha256txtPage () { - return $(this.__download_official_release_zip_sha256_txt_page__) - } - - get downloadOfficialReleaseZipSha256txtPageTitle () { - return $(this.__download_official_release_zip_sha256_txt_page_title__) - } - - get downloadOfficialReleaseZipSha256txtPageSubtitle () { - return $(this.__download_official_release_zip_sha256_txt_page_subtitle__) - } - - get downloadOfficialReleaseZipSha256txtPageProgress () { - return $(this.__download_official_release_zip_sha256_txt_page_progress__) - } - - get checkingReleaseZipSigMsg () { - return $(this.__checking_release_zip_sig_msg__) - } - - get notFoundReleaseZipSigMsg () { - return $(this.__not_found_release_zip_sig_msg__) - } - - get foundReleaseZipSigMsg () { - return $(this.__found_release_zip_sig_msg__) - } - - get downloadOfficialReleaseZipSigPage () { - return $(this.__download_official_release_zip_sig_page__) - } - - get downloadOfficialReleaseZipSigTitle () { - return $(this.__download_official_release_zip_sig_title__) - } - - get downloadOfficialReleaseZipSigSubtitle () { - return $(this.__download_official_release_zip_sig_subtitle__) - } - - get downloadOfficialReleaseZipSigProgress () { - return $(this.__download_official_release_zip_sig_progress__) - } - - get downloadOfficialReleasePemPage () { - return $(this.__download_official_release_pem_page__) - } - - get downloadOfficialReleasePemTitle () { - return $(this.__download_official_release_pem_title__) - } - - get downloadOfficialReleasePemSubtitle () { - return $(this.__download_official_release_pem_subtitle__) - } - - get downloadOfficialReleasePemProgress () { - return $(this.__download_official_release_pem_page__) - } - - get checkingReleasePemMsg () { - return $(this.__checking_release_pem_msg__) - } - - get notFoundReleasePemMsg () { - return $(this.__not_found_release_pem_msg__) - } - - get foundReleasePemMsg () { - return $(this.__found_release_pem_msg__) - } - - get checkVerifyOfficialReleasePage () { - return $(this.__check_verify_official_release_page__) - } - - get checkVerifyOfficialReleaseLenSassamanIsUsingOpenssl (){ - return $(this.__check_verify_official_release_len_sassaman_is_using_openssl__) - } - - get verifiedOfficialReleasePage () { - return $(this.__verified_official_release_page__) - } - - get verifiedOfficialReleasePageSha2256IntegrityTitle () { - return $(this.__verified_official_release_page_sha256_integrity_title__) - } - - get verifiedOfficialReleasePageSha2256IntegritySha256txt () { - return $(this.__verified_official_release_page_sha256_integrity_txt__) - } - - get verifiedOfficialReleasePageSha2256IntegritySha256 () { - return $(this.__verified_official_release_page_sha256_integrity__) - } - - get verifiedOfficialReleasePageSignatureTitle () { - return $(this.__verified_official_release_page_signature_title__) - } - - get verifiedOfficialReleasePageSignatureCommand () { - return $(this.__verified_official_release_page_signature_command__) - } - - get verifiedOfficialReleasePageSignatureResult () { - return $(this.__verified_official_release_page_signature_result__) - } - - get verifiedOfficialReleasePageBackButton () { - return $(this.__verified_official_release_page_back_button__) - } - -} - -module.exports = App diff --git a/test/e2e/pageobjects/page.ts b/test/e2e/pageobjects/page.ts deleted file mode 100644 index 0569c56f..00000000 --- a/test/e2e/pageobjects/page.ts +++ /dev/null @@ -1,12 +0,0 @@ -const { $ } = require('@wdio/globals') - -class Page { - - protected __main__: string - - get title (): typeof $ { - return $('title') - } -} - -module.exports = Page diff --git a/test/e2e/specs/000.create-config.spec.mts b/test/e2e/specs/000.create-config.spec.mts deleted file mode 100644 index 64c785f8..00000000 --- a/test/e2e/specs/000.create-config.spec.mts +++ /dev/null @@ -1,35 +0,0 @@ -import { readdir, readFile } from 'fs/promises' -import { join } from 'path' -import { expect } from 'chai' -import { browser } from '@wdio/globals' -import { describe, it } from 'mocha' - -describe('KruxInstaller configuration', () => { - - it('\'appData\' property should be a path that exist within \'krux-installer\' directory', async () => { - const appData = await browser.electron.execute(function (electron) { - return electron.app.getPath('appData') - }) - const list = await readdir(appData) - expect(list.includes('krux-installer')).to.be.true - }) - - it('\'config.json\' file should exists inside \'krux-installer\' directory', async () => { - const appData = await browser.electron.execute(function (electron) { - return electron.app.getPath('appData') - }) - const dir = join(appData, 'krux-installer') - const list = await readdir(dir) - expect(list.includes('config.json')).to.be.true - }) - - it('\'config.json\' should be a readable string', async () => { - const appData = await browser.electron.execute(function (electron) { - return electron.app.getPath('appData') - }) - const filePath = join(appData, 'krux-installer', 'config.json') - const file = await readFile(filePath, 'utf-8') - expect(file).to.be.a('string') - }) - -}) diff --git a/test/e2e/specs/001.check-config.spec.mts b/test/e2e/specs/001.check-config.spec.mts deleted file mode 100644 index 089f03cf..00000000 --- a/test/e2e/specs/001.check-config.spec.mts +++ /dev/null @@ -1,89 +0,0 @@ -import { expect } from 'chai' -import { browser } from '@wdio/globals' -import { describe, it } from 'mocha' -import { createRequire } from 'module' -const { version } = createRequire(import.meta.url)('../../../package.json') - -describe('Check created configuration', () => { - - it('\'appVersion\' property should be equal to the value defined in package.json', async () => { - const appVersion = await browser.electron.execute(function (electron) { - return electron.app.getVersion() - }) - expect(appVersion).to.equal(version) - }) - - it('\'resources\' property should be properly set for the platform', async () => { - const resources = await browser.electron.execute(function (electron) { - return electron.app.store.get('resources') - }) - - let regexp: RegExp - - if (process.env.CI && process.env.GITHUB_ACTION) { - if (process.platform === 'linux') { - regexp = new RegExp(`/home/[a-zA-Z0-9/]+`, 'g') - expect(resources).to.match(regexp) - } - if (process.platform === 'win32') { - regexp = /[A-Z]:[a-zA-Z\\-]+/g - expect(resources).to.match(regexp as RegExp) - } - if (process.platform === 'darwin') { - regexp = new RegExp('/Users/[a-zA-Z0-9/-]+', 'g') - expect(resources).to.match(regexp as RegExp) - } - } else { - const docs = 'Documents|Documentos|Documenten|Documenti|Unterlagen' - if (process.platform === 'linux') { - regexp = new RegExp(`/home/[a-zA-Z0-9/]+/(${docs})`, 'g') - expect(resources).to.match(regexp as RegExp) - } - if (process.platform === 'win32') { - regexp = new RegExp(`[A-Z]:\\Users\\[a-zA-Z0-9]+\\(${docs})\\${name}`, 'g') - expect(resources).to.match(regexp as RegExp) - } - if (process.platform === 'darwin') { - regexp = new RegExp(`/Users/[a-zA-Z0-9\/\-]+/(${docs})/${name}`, 'g') - expect(resources).to.match(regexp as RegExp) - } - } - }) - - it('\'os\' property should be properly set for the platform', async () => { - const os = await browser.electron.execute(function (electron) { - return electron.app.store.get('os') - }) - expect(os).to.equal(process.platform) - }) - - it('\'versions\' property should be an Array with 0 elements', async () => { - const versions = await browser.electron.execute(function (electron) { - return electron.app.store.get('versions') - }) - expect(versions).to.be.an('Array') - expect(versions.length).to.equal(0) - }) - - it('\'version\' property should be equal to \'Select version\'', async () => { - const version = await browser.electron.execute(function (electron) { - return electron.app.store.get('version') - }) - expect(version).to.equal('Select version') - }) - - it('\'device\' property should be equal to \'Select device\'', async () => { - const device = await browser.electron.execute(function (electron) { - return electron.app.store.get('device') - }) - expect(device).to.equal('Select device') - }) - - it('\'device\' property should be equal to \'Select device\'', async () => { - const showFlash = await browser.electron.execute(function (electron) { - return electron.app.store.get('showFlash') - }) - expect(showFlash).to.equal(false) - }) - -}) diff --git a/test/e2e/specs/002.app-startup.spec.mts b/test/e2e/specs/002.app-startup.spec.mts deleted file mode 100644 index de838cf4..00000000 --- a/test/e2e/specs/002.app-startup.spec.mts +++ /dev/null @@ -1,28 +0,0 @@ -import { expect } from 'chai' -import { browser } from '@wdio/globals' -import { describe, it } from 'mocha' - -describe('KruxInstaller start up', () => { - - it('should be ready', async () => { - const isReady = await browser.electron.execute(function (electron) { - return electron.app.isReady() - }) - expect(isReady).to.be.equal(true) - }) - - it('application name should be correct', async () => { - const name = await browser.electron.execute(function (electron) { - return electron.app.getName() - }) - expect(name).to.be.equal('krux-installer') - }) - - it('application version should be correct', async () => { - const version = await browser.electron.execute(function (electron) { - return electron.app.getVersion() - }) - expect(version).to.be.equal('0.0.13') - }) - -}) diff --git a/test/e2e/specs/003.app-logo.spec.mts b/test/e2e/specs/003.app-logo.spec.mts deleted file mode 100644 index ed434009..00000000 --- a/test/e2e/specs/003.app-logo.spec.mts +++ /dev/null @@ -1,51 +0,0 @@ -import { expect } from '@wdio/globals' -import { describe, it } from 'mocha' -import { createRequire } from 'module' - -const App = createRequire(import.meta.url)('../pageobjects/app.page') - -// When wdio gets the generated -//
 html tag from vue-ascii-morph,
-// its change the first and last string
-const KRUX_INSTALLER_LOGO = [
-    "██           ",
-    "       ██           ",
-    "       ██           ",
-    "     ██████         ",
-    "       ██           ",
-    "       ██  ██       ",
-    "       ██ ██        ",
-    "       ████         ",
-    "       ██ ██        ",
-    "       ██  ██       ",
-    "       ██   ██      ",
-    "                    ",
-    "   KRUX INSTALLER"
-].join('\n')
-
-describe('KruxInstaller initialization', () => {
-
-  let instance: typeof App;
-
-  before(function () {
-    instance = new App()
-  })
-
-  it('\'#app\' html tag should exist', async () => { 
-    await instance.app.waitForExist({ timeout: 5000 })
-  })
-
-  it('\'#main\' html tag should exist', async () => { 
-    await instance.main.waitForExist({ timeout: 5000 })
-  })
-
-  it('krux-installer logo should appears', async () => { 
-    await instance.logo.waitForExist({ timeout: 3000 })
-    await expect(instance.logo).toBeDisplayed()
-    await expect(instance.logo).toHaveText(KRUX_INSTALLER_LOGO)
-  })
-
-  it('krux-installer logo should disappear', async () => { 
-    await instance.logo.waitForExist({ timeout: 3000, reverse: true })
-  })
-})
diff --git a/test/e2e/specs/004.app-consoleload-before-main-page.spec.mts b/test/e2e/specs/004.app-consoleload-before-main-page.spec.mts
deleted file mode 100644
index b5fc16d6..00000000
--- a/test/e2e/specs/004.app-consoleload-before-main-page.spec.mts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-
-// When wdio gets the generated
-// 
 html tag from vue-ascii-morph,
-// its change the first and last string
-const LOADING_DATA_MESSAGE: string = 'Loading data from storage'
-const VERIFYING_OPENSSL_MESSAGE: string = 'Verifying openssl'
-const OPENSSL_FOUND_LINUX_MESSAGE: string = 'openssl for linux found'
-const OPENSSL_FOUND_DARWIN_MESSAGE: string = 'openssl for darwin found'
-const OPENSSL_FOUND_WIN32_MESSAGE: string = 'openssl for win32 found'
-
-describe('KruxInstaller loading messages', () => {
-
-  let instance: any;
-
-  before(function () {
-    instance = new App()
-  })
-
-  it('should \'Loading data from storage\' message appears', async () => {
-    await instance.loadingDataMsg.waitForExist({ timeout: 3000 })
-    await expect(instance.loadingDataMsg).toBeDisplayed()
-    await expect(instance.loadingDataMsg).toHaveText(LOADING_DATA_MESSAGE)
-  })
-
-  it('should \'Verifying openssl\' message appears', async () => {
-    await instance.verifyingOpensslMsg.waitForExist({ timeout: 6000 })
-    await expect(instance.verifyingOpensslMsg).toBeDisplayed()
-    await expect(instance.verifyingOpensslMsg).toHaveText(VERIFYING_OPENSSL_MESSAGE)
-  })
-
-  it('should \'openssl for  found\' message appears', async () => {
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist({ timeout: 9000 })
-      await expect(instance.opensslForLinuxFound).toBeDisplayed()
-      await expect(instance.opensslForLinuxFound).toHaveText(OPENSSL_FOUND_LINUX_MESSAGE)
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist({ timeout: 9000 })
-      await expect(instance.verifyingOpensslMsg).toBeDisplayed()
-      await expect(instance.opensslForDarwinFound).toHaveText(OPENSSL_FOUND_DARWIN_MESSAGE)
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist({ timeout: 9000 })
-      await expect(instance.verifyingOpensslMsg).toBeDisplayed()
-      await expect(instance.opensslForWin32Found).toHaveText(OPENSSL_FOUND_WIN32_MESSAGE)
-    }
-    
-  })
-
-  it('should messages disappears', async () => {
-    await instance.loadingDataMsg.waitForExist({ timeout: 12000, reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ timeout: 12000, reverse: true })
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist({ timeout: 12000, reverse: true  })
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist({ timeout: 12000, reverse: true  })
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist({ timeout: 12000, reverse: true })
-    }
-  })
-})
diff --git a/test/e2e/specs/005.main-menu.spec.mts b/test/e2e/specs/005.main-menu.spec.mts
deleted file mode 100644
index 72ca34e8..00000000
--- a/test/e2e/specs/005.main-menu.spec.mts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-const SELECT_DEVICE_TEXT = 'Select device'
-const SELECT_VERSION_TEXT = 'Select version'
-
-describe('KruxInstaller Main page', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-  })
-
-  it('should main page appears', async () => {
-    await expect(instance.mainPage).toBeDisplayed()
-  })
-
-  it('should message text not appears', async () => {
-    await expect(instance.mainClickMessageText).not.toBeDisplayed()
-  })
-
-  it('should \'Select device\' button appears', async () => {
-    await expect(instance.mainSelectDeviceButton).toBeDisplayed()
-  })
-
-  it('should \'Select version\' button appears', async () => {
-    await expect(instance.mainSelectVersionButton).toBeDisplayed()
-  })
-
-  it('should \'Select device\' button have \'Select device\' text', async () => {
-    await expect(instance.mainSelectDeviceText).toHaveText(SELECT_DEVICE_TEXT)
-  })
-
-  it('should \'Select version\' button have \'Select version\' text', async () => {
-    await expect(instance.mainSelectVersionText).toHaveText(SELECT_VERSION_TEXT)
-  })
-
-  it('should \'Flash\' button not appears', async () => {
-    await expect(instance.mainSelectFlashButton).not.toBeDisplayed()
-  })
-})
diff --git a/test/e2e/specs/006.select-device-show-only.spec.mts b/test/e2e/specs/006.select-device-show-only.spec.mts
deleted file mode 100644
index 83619a39..00000000
--- a/test/e2e/specs/006.select-device-show-only.spec.mts
+++ /dev/null
@@ -1,90 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectDevice page (show only)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-  })
-
-  it('should click on \'Select device\' button and page change', async () => {
-    await instance.mainSelectDeviceButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.selectDevicePage.waitForExist()
-    await expect(instance.selectDevicePage).toBeDisplayed()
-  })
-
-  it('should \'maixpy_m5stickv\' button be displayed', async () => {
-    await instance.selectMaixpyM5StickVButton.waitForExist()
-    await expect(instance.selectMaixpyM5StickVButton).toBeDisplayed()
-  })
-
-  it('should \'maixpy_m5stickv\' button have \'maixpy_m5stickv\' text', async () => { 
-    await instance.selectMaixpyM5StickVText.waitForExist()
-    await expect(instance.selectMaixpyM5StickVText).toHaveText('maixpy_m5stickv')
-  })
-
-  it('should \'maixpy_amigo\' button be displayed', async () => {
-    await instance.selectMaixpyAmigoButton.waitForExist()
-    await expect(instance.selectMaixpyAmigoButton).toBeDisplayed()
-  })
-
-  it('should \'maixpy_amigo\' button have \'maixpy_amigo\' text', async () => { 
-    await instance.selectMaixpyAmigoText.waitForExist()
-    await expect(instance.selectMaixpyAmigoText).toHaveText('maixpy_amigo')
-  })
-
-  it('should \'maixpy_bit\' button be displayed', async () => {
-    await instance.selectMaixpyBitButton.waitForExist()
-    await expect(instance.selectMaixpyBitButton).toBeDisplayed()
-  })
-
-  it('should \'maixpy_bit\' button have \'maixpy_bit\' text', async () => { 
-    await instance.selectMaixpyBitText.waitForExist()
-    await expect(instance.selectMaixpyBitText).toHaveText('maixpy_bit')
-  })
-
-  it('should \'maixpy_dock\' button be displayed', async () => {
-    await instance.selectMaixpyDockButton.waitForExist()
-    await expect(instance.selectMaixpyDockButton).toBeDisplayed()
-  })
-
-  it('should \'maixpy_dock\' button have \'maixpy_dock\' text', async () => { 
-    await instance.selectMaixpyDockText.waitForExist()
-    await expect(instance.selectMaixpyDockText).toHaveText('maixpy_dock')
-  })
-
-  it('should \'back\' button be displayed', async () => {
-    await instance.selectBackButton.waitForExist()
-    await expect(instance.selectBackButton).toBeDisplayed()
-  })
-
-  it('should \'back\' button have \'back\' text', async () => { 
-    await instance.selectBackText.waitForExist()
-    await expect(instance.selectBackText).toHaveText('Back')
-  })
-})
diff --git a/test/e2e/specs/007.select-device-m5stickv.spec.mts b/test/e2e/specs/007.select-device-m5stickv.spec.mts
deleted file mode 100644
index 7186c68e..00000000
--- a/test/e2e/specs/007.select-device-m5stickv.spec.mts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectDevice page selects \'maixpy_m5stickv\' device', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectDeviceButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.selectDevicePage.waitForExist() 
-    await instance.selectMaixpyM5StickVButton.waitForExist() 
-    await instance.selectMaixpyM5StickVButton.click()
-  })
-
-  it('should change to Main page', async () => {
-    await instance.selectDevicePage.waitForExist({ reverse: true })
-    await expect(instance.selectDevicePage).not.toBeDisplayed()
-    await instance.mainPage.waitForExist()
-    await expect(instance.mainPage).toBeDisplayed()
-  })
-
-  it('should \'Select device\' button changed its text to \'Device: maixpy_m5stickv\'', async () => {
-    await expect(instance.mainSelectDeviceText).toHaveText('Device: maixpy_m5stickv')
-  })
-
-})
diff --git a/test/e2e/specs/008.select-device-amigo.spec.mts b/test/e2e/specs/008.select-device-amigo.spec.mts
deleted file mode 100644
index b5aadbbe..00000000
--- a/test/e2e/specs/008.select-device-amigo.spec.mts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectDevice page selects \'maixpy_amigo\' device', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectDeviceButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.selectDevicePage.waitForExist() 
-    await instance.selectMaixpyAmigoButton.waitForExist() 
-    await instance.selectMaixpyAmigoButton.click()
-  })
-
-  it('should change to Main page', async () => {
-    await instance.selectDevicePage.waitForExist({ reverse: true })
-    await expect(instance.selectDevicePage).not.toBeDisplayed()
-    await instance.mainPage.waitForExist()
-    await expect(instance.mainPage).toBeDisplayed()
-  })
-
-  it('should \'Select device\' button changed its text to \'Device: maixpy_amigo\'', async () => {
-    await expect(instance.mainSelectDeviceText).toHaveText('Device: maixpy_amigo')
-  })
-
-})
diff --git a/test/e2e/specs/009.select-device-bit.spec.mts b/test/e2e/specs/009.select-device-bit.spec.mts
deleted file mode 100644
index 7977c500..00000000
--- a/test/e2e/specs/009.select-device-bit.spec.mts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectDevice page selects \'maixpy_bit\' device', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectDeviceButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.selectDevicePage.waitForExist() 
-    await instance.selectMaixpyBitButton.waitForExist() 
-    await instance.selectMaixpyBitButton.click()
-  })
-
-  it('should change to Main page', async () => {
-    await instance.selectDevicePage.waitForExist({ reverse: true })
-    await expect(instance.selectDevicePage).not.toBeDisplayed()
-    await instance.mainPage.waitForExist()
-    await expect(instance.mainPage).toBeDisplayed()
-  })
-
-  it('should \'Select device\' button changed its text to \'Device: maixpy_bit\'', async () => {
-    await expect(instance.mainSelectDeviceText).toHaveText('Device: maixpy_bit')
-  })
-
-})
diff --git a/test/e2e/specs/010.select-device-dock.spec.mts b/test/e2e/specs/010.select-device-dock.spec.mts
deleted file mode 100644
index 986fbbef..00000000
--- a/test/e2e/specs/010.select-device-dock.spec.mts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectDevice page selects \'maixpy_dock\' device', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectDeviceButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.selectDevicePage.waitForExist() 
-    await instance.selectMaixpyDockButton.waitForExist() 
-    await instance.selectMaixpyDockButton.click()
-  })
-
-  it('should change to Main page', async () => {
-    await instance.selectDevicePage.waitForExist({ reverse: true })
-    await expect(instance.selectDevicePage).not.toBeDisplayed()
-    await instance.mainPage.waitForExist()
-    await expect(instance.mainPage).toBeDisplayed()
-  })
-
-  it('should \'Select device\' button changed its text to \'Device: maixpy_dock\'', async () => {
-    await expect(instance.mainSelectDeviceText).toHaveText('Device: maixpy_dock')
-  })
-
-})
\ No newline at end of file
diff --git a/test/e2e/specs/011.select-device-back.spec.mts b/test/e2e/specs/011.select-device-back.spec.mts
deleted file mode 100644
index 8442f1cd..00000000
--- a/test/e2e/specs/011.select-device-back.spec.mts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectDevice page selects \'back\' button', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectDeviceButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.selectDevicePage.waitForExist() 
-    await instance.selectBackButton.waitForExist() 
-    await instance.selectBackButton.click()
-  })
-
-  it('should change to Main page', async () => {
-    await instance.selectDevicePage.waitForExist({ reverse: true })
-    await expect(instance.selectDevicePage).not.toBeDisplayed()
-    await instance.mainPage.waitForExist()
-    await expect(instance.mainPage).toBeDisplayed()
-  })
-
-  it('should \'Select device\' button mantain its text to \'Select device\'', async () => {
-    await expect(instance.mainSelectDeviceText).toHaveText('Select device')
-  })
-
-})
diff --git a/test/e2e/specs/012.select-version-show-only.spec.mts b/test/e2e/specs/012.select-version-show-only.spec.mts
deleted file mode 100644
index 992a7715..00000000
--- a/test/e2e/specs/012.select-version-show-only.spec.mts
+++ /dev/null
@@ -1,94 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-const GITHUB_OCTOCAT = [ 
-    "MMM.           .MMM        ",
-    "       MMMMMMMMMMMMMMMMMMM        ",
-    "       MMMMMMMMMMMMMMMMMMM        ",  
-    "      MMMMMMMMMMMMMMMMMMMMM       ",
-    "     MMMMMMMMMMMMMMMMMMMMMMM      ",
-    "    MMMMMMMMMMMMMMMMMMMMMMMMM     ",
-    "    MMMM::- -:::::::- -::MMMM     ",
-    "     MM~:~ 00~:::::~ 00~:~MM      ",
-    ".. MMMMM::.00:::+:::.00::MMMMM .. ",
-    "      .MM::::: ._. :::::MM.       ",
-    "         MMMM;:::::;MMMM          ",
-    "  -MM        MMMMMMM              ",
-    "  ^  M+     MMMMMMMMM             ",
-    "      MMMMMMM MM MM MM            ",
-    "           MM MM MM MM            ",
-    "           MM MM MM MM            ",
-    "        .~~MM~MM~MM~MM~~.         ",
-    "     ~~~~MM:~MM~~~MM~:MM~~~~      ",
-    "    ~~~~~~==~==~~~==~==~~~~~~     ",
-    "     ~~~~~~==~==~==~==~~~~~~      ",
-    "         :~==~==~==~==~~          ",
-    " Checking latest release on github"
-  ].join('\n')
-
-describe('KruxInstaller SelectVersion page (show only)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-  })
-
-  it('should click on \'Select version\' button and page change', async () => {
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-  })
-
-  it('should github\'s ocotcat checker appears', async () => { 
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await expect(instance.githubOctocatCheckerLogo).toBeDisplayed()
-    await expect(instance.githubOctocatCheckerLogo).toHaveText(GITHUB_OCTOCAT)
-  })
-
-  it('should \'Select version\' page appear', async () => {
-    await instance.selectVersionPage.waitForExist()
-    await expect(instance.selectVersionPage).toBeDisplayed()
-  })
-  
-  it('should \'selfcustody/krux/releases/tag/v24.03.0\' button appear', async () => {
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await expect(instance.selectVersionSelfcustodyButton).toBeDisplayed()
-  })
-
-  it('should \'selfcustody/krux/releases/tag/v24.03.0\' button have \'selfcustody/krux/releases/tag/v24.03.0\' text', async () => {
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await expect(instance.selectVersionSelfcustodyText).toHaveText('selfcustody/krux/releases/tag/v24.03.0')
-  })
-
-  it('should \'odudex/krux_binaries\' button appear', async () => {
-    await instance.selectVersionOdudexButton.waitForExist()
-    await expect(instance.selectVersionOdudexButton).toBeDisplayed()
-  })
-
-  it('should \'odudex\/krux_binaries\' button have \'odudex\/krux_binaries\' text', async () => {
-    await instance.selectVersionOdudexText.waitForExist()
-    await expect(instance.selectVersionOdudexText).toHaveText('odudex/krux_binaries')
-  })
-})
diff --git a/test/e2e/specs/013.select-version-selfcustody-release-zip.spec.mts b/test/e2e/specs/013.select-version-selfcustody-release-zip.spec.mts
deleted file mode 100644
index 8b534dfe..00000000
--- a/test/e2e/specs/013.select-version-selfcustody-release-zip.spec.mts
+++ /dev/null
@@ -1,94 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectVersion page (download release)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await instance.selectVersionPage.waitForExist()
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await instance.selectVersionOdudexButton.waitForExist()
-    await instance.selectVersionOdudexText.waitForExist()
-  })
-
-  it('should click on \'selfcustody/krux/tags/v24.03.0\' and go to ConsoleLoad page', async () => {
-    await instance.selectVersionSelfcustodyButton.click()
-    await instance.selectVersionPage.waitForExist({ reverse: true })
-    await expect(instance.selectVersionPage).not.toBeDisplayed()
-  })
-
-  it('should \'Checking v24.03.0/krux-v24.03.0.zip\' message appears', async () => {
-    await instance.checkingReleaseZipMsg.waitForExist()
-    await expect(instance.checkingReleaseZipMsg).toBeDisplayed()
-    await expect(instance.checkingReleaseZipMsg).toHaveText('Checking v24.03.0/krux-v24.03.0.zip')
-  })
-
-  it('should \'v24.03.0/krux-v24.03.0.zip not found\' message appears', async () => {
-    await instance.notFoundReleaseZipMsg.waitForExist()
-    await expect(instance.notFoundReleaseZipMsg).toBeDisplayed()
-    if (process.platform === 'linux' || process.platform === 'darwin') {
-      await expect(instance.notFoundReleaseZipMsg).toHaveText('v24.03.0/krux-v24.03.0.zip not found')
-    } else if (process.platform === 'win32') {
-      await expect(instance.notFoundReleaseZipMsg).toHaveText('v24.03.0\\krux-v24.03.0.zip not found')
-    }
-  })
-
-  it('should go to DownloadOfficialReleaseZip page', async () => {
-    await instance.downloadOfficialReleaseZipPage.waitForExist()
-    await expect(instance.downloadOfficialReleaseZipPage).toBeDisplayed()
-  })
-
-  it('should DownloadOfficialReleaseZip page have \'Downloading\' title', async () => {
-    await instance.downloadOfficialReleaseZipTitle.waitForExist()
-    await expect(instance.downloadOfficialReleaseZipTitle).toBeDisplayed()
-    await expect(instance.downloadOfficialReleaseZipTitle).toHaveText('Downloading')
-  })
-
-  it('should DownloadOfficialReleaseZip page have \'https://github.com/selfcustody/krux/releases/download/v24.03.0/krux-v24.03.0.zip\' subtitle', async () => {
-    await instance.downloadOfficialReleaseZipTitle.waitForExist()
-    await expect(instance.downloadOfficialReleaseZipSubtitle).toBeDisplayed()
-    await expect(instance.downloadOfficialReleaseZipSubtitle).toHaveText('https://github.com/selfcustody/krux/releases/download/v24.03.0/krux-v24.03.0.zip')
-  })
-
-  it('should DownloadOfficialReleaseZip page progress until 100%', async () => {
-    await instance.downloadOfficialReleaseZipProgress.waitForExist()
-    await expect(instance.downloadOfficialReleaseZipProgress).toBeDisplayed()
-    await instance.downloadOfficialReleaseZipProgress.waitUntil(async function () {
-      const percentText = await this.getText()
-      const percent = parseFloat(percentText.split('%')[0])
-      return percent === 100.00
-    }, {
-      timeout: 600000,
-      interval: 50
-    })
-  })
-
-})
diff --git a/test/e2e/specs/014.already-downloaded-selfcustody-release-zip.spec.mts b/test/e2e/specs/014.already-downloaded-selfcustody-release-zip.spec.mts
deleted file mode 100644
index 264bb685..00000000
--- a/test/e2e/specs/014.already-downloaded-selfcustody-release-zip.spec.mts
+++ /dev/null
@@ -1,102 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectVersion page (already downloaded release - show only)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await instance.selectVersionPage.waitForExist()
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await instance.selectVersionOdudexButton.waitForExist()
-    await instance.selectVersionOdudexText.waitForExist()
-    await instance.selectVersionSelfcustodyButton.click()
-    await instance.selectVersionPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipMsg.waitForExist()
-  })
-
-  it('should \'v24.03.0/krux-v24.03.0.zip found\' message appears', async () => {
-    await instance.foundReleaseZipMsg.waitForExist()
-    await expect(instance.foundReleaseZipMsg).toBeDisplayed()
-    if (process.platform === 'linux' || process.platform === 'darwin') {
-      await expect(instance.foundReleaseZipMsg).toHaveText('v24.03.0/krux-v24.03.0.zip found')
-    } else if (process.platform === 'win32') {
-      await expect(instance.foundReleaseZipMsg).toHaveText('v24.03.0\\krux-v24.03.0.zip found')
-    }
-  })
-
-  it('should WarningDownload page should be displayed', async () => {
-    await instance.warningDownloadPage.waitForExist()
-    await expect(instance.warningDownloadPage).toBeDisplayed()
-  }) 
-
-  it('should \'v24.03.0/krux-v24.03.0.zip already downloaded\' message be displayed', async () => {
-    await instance.warningAlreadyDownloadedText.waitForExist()
-    await expect(instance.warningAlreadyDownloadedText).toBeDisplayed()
-    if (process.platform === 'linux' || process.platform === 'darwin') {
-      await expect(instance.warningAlreadyDownloadedText).toHaveText('v24.03.0/krux-v24.03.0.zip already downloaded')
-    } else if (process.platform === 'win32') {
-      await expect(instance.warningAlreadyDownloadedText).toHaveText('v24.03.0\\krux-v24.03.0.zip already downloaded')
-    }
-  })
-
-  it('should \'Proceed with current file\' button be displayed', async () => {
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await expect(instance.warningDownloadProceedButton).toBeDisplayed()
-    await expect(instance.warningDownloadProceedButtonText).toBeDisplayed()
-    await expect(instance.warningDownloadProceedButtonText).toHaveText('Proceed with current file')
-  })
-
-  it('should \'Download it again\' button be displayed', async () => {
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await expect(instance.warningDownloadAgainButton).toBeDisplayed()
-    await expect(instance.warningDownloadAgainButtonText).toBeDisplayed()
-    await expect(instance.warningDownloadAgainButtonText).toHaveText('Download it again')
-  })
-
-  it('should \'Show details\' button be displayed', async () => {
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await expect(instance.warningDownloadShowDetailsButton).toBeDisplayed()
-    await expect(instance.warningDownloadShowDetailsButtonText).toBeDisplayed()
-    await expect(instance.warningDownloadShowDetailsButtonText).toHaveText('Show details')
-  })
-
-  it('should \'Back\' button be displayed', async () => {
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-    await expect(instance.warningDownloadBackButton).toBeDisplayed()
-    await expect(instance.warningDownloadBackButtonText).toBeDisplayed()
-    await expect(instance.warningDownloadBackButtonText).toHaveText('Back')
-  })
-
-})
diff --git a/test/e2e/specs/015.already-downloaded-selfcustody-release-zip-click-back-button.spec.mts b/test/e2e/specs/015.already-downloaded-selfcustody-release-zip-click-back-button.spec.mts
deleted file mode 100644
index 60f1af86..00000000
--- a/test/e2e/specs/015.already-downloaded-selfcustody-release-zip-click-back-button.spec.mts
+++ /dev/null
@@ -1,72 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectVersion page (already downloaded release - click back button)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await instance.selectVersionPage.waitForExist()
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await instance.selectVersionOdudexButton.waitForExist()
-    await instance.selectVersionOdudexText.waitForExist()
-    await instance.selectVersionSelfcustodyButton.click()
-    await instance.selectVersionPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipMsg.waitForExist()
-    await instance.foundReleaseZipMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningAlreadyDownloadedText.waitForExist() 
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-  })
-
-  it ('should click \'Back\' button and go out from WarningDownload page', async () => {
-    await instance.warningDownloadBackButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await expect(instance.warningDownloadPage).not.toBeDisplayed()
-  })
-
-  it('should Main Page be displayed', async () => {
-    await instance.mainPage.waitForExist()
-    await expect(instance.mainPage).toBeDisplayed()
-  })
-
-  it('should \'Select version\' button changed to \'Version: selfcustody/krux/releases/tag/v24.03.0\'', async () => {
-    await instance.mainSelectVersionText.waitForExist()
-    await expect(instance.mainSelectVersionText).toBeDisplayed()
-    await expect(instance.mainSelectVersionText).toHaveText('Version: selfcustody/krux/releases/tag/v24.03.0')
-  })
-})
diff --git a/test/e2e/specs/016.already-downloaded-selfcustody-release-zip-click-show-details-button.spec.mts b/test/e2e/specs/016.already-downloaded-selfcustody-release-zip-click-show-details-button.spec.mts
deleted file mode 100644
index 953ee725..00000000
--- a/test/e2e/specs/016.already-downloaded-selfcustody-release-zip-click-show-details-button.spec.mts
+++ /dev/null
@@ -1,134 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { join } from 'path'
-import { homedir } from 'os'
-import { osLangSync } from 'os-lang'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectVersion page (already downloaded release - click show details button)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await instance.selectVersionPage.waitForExist()
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await instance.selectVersionOdudexButton.waitForExist()
-    await instance.selectVersionOdudexText.waitForExist()
-    await instance.selectVersionSelfcustodyButton.click()
-    await instance.selectVersionPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipMsg.waitForExist()
-    await instance.foundReleaseZipMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningAlreadyDownloadedText.waitForExist() 
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-  })
-
-  it ('should overlay not be shown', async () => {
-    await expect(instance.warningAlreadyDownloadedOverlay).not.toBeDisplayed()
-  })
-
-  it ('should click \'Show details\' button and overlay must be visible', async () => {
-    await instance.warningDownloadShowDetailsButton.click()
-    await instance.warningAlreadyDownloadedOverlay.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlay).toBeDisplayed()
-  })
-
-  it ('should overlay title be \'Resource details\'', async () => {
-    await instance.warningAlreadyDownloadedOverlayTitle.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlayTitle).toBeDisplayed()
-    await expect(instance.warningAlreadyDownloadedOverlayTitle).toHaveText('Resource details')
-  })
-
-  it ('should overlay subtitle be \'v24.03.0/krux-v24.03.0.zip\'', async () => {
-    await instance.warningAlreadyDownloadedOverlayTitle.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlaySubtitle).toBeDisplayed()
-    await expect(instance.warningAlreadyDownloadedOverlaySubtitle).toHaveText('v24.03.0/krux-v24.03.0.zip')
-  })
-
-  it ('should a overlay text have \'Remote: https://github.com/selfcustody/krux/releases/download/v24.03.0/krux-v24.03.0.zip\'', async () => {
-    await instance.warningAlreadyDownloadedOverlayTextRemote.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlayTextRemote).toBeDisplayed()
-    await expect(instance.warningAlreadyDownloadedOverlayTextRemote).toHaveText('Remote:\nhttps://github.com/selfcustody/krux/releases/download/v24.03.0/krux-v24.03.0.zip')
-  })
-
-  it ('should a overlay text have properly local resource', async () => {
-    await instance.warningAlreadyDownloadedOverlayTextLocal.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlayTextLocal).toBeDisplayed()
-    
-    let resources = ''
-    if (process.env.CI && process.env.GITHUB_ACTION) {
-      if (process.platform  === 'linux') {
-        resources = '/home/runner/krux-installer'
-      } else if (process.platform  === 'win32') {
-        resources = 'C:\\Users\\runneradmin\\Documents\\krux-installer'
-      } else if (process.platform  === 'darwin') {
-        resources = '/Users/runner/Documents/krux-installer'
-      }
-    } else {
-      const lang = osLangSync()
-      const home = homedir()
-      if ( lang.match(/en-*/g)) {
-        resources = join(home, 'Documents', 'krux-installer')
-      } else if ( lang.match(/pt-*/g)) {
-        resources = join(home, 'Documentos', 'krux-installer')
-      } else {
-        throw new Error(`${lang} not implemented. Please implement it with correct \'Documents\' folder name`)
-      }
-    }
-
-    const resource = join(resources, 'v24.03.0', 'krux-v24.03.0.zip')
-    await expect(instance.warningAlreadyDownloadedOverlayTextLocal).toHaveText(`Local:\n${resource}`)
-  })
-
-  it('should a overlay text have the properly description', async () => {
-    await instance.warningAlreadyDownloadedOverlayTextWhatdo.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlayTextWhatdo).toBeDisplayed()
-    await expect(instance.warningAlreadyDownloadedOverlayTextWhatdo).toHaveText('Description:\nThis file is the official release with all necessary contents to flash or update krux firmware on your Kendryte K210 device, including the firmware signature that prove the firmware\'s authenticity')
-  })
-
-  it('should \'close\' have \'Close\' text',async () => {
-    await instance.warningAlreadyDownloadedOverlayButtonClose.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlayButtonClose).toBeDisplayed()
-    await expect(instance.warningAlreadyDownloadedOverlayButtonClose).toHaveText('Close')
-  })
-
-  it('should \'close\' button make overlay not visible', async () => {
-    await instance.warningAlreadyDownloadedOverlayButtonClose.click()
-    await instance.warningAlreadyDownloadedOverlay.waitForExist({ reverse: true })
-    await expect(instance.warningAlreadyDownloadedOverlay).not.toBeDisplayed()
-  })
-  
-})
diff --git a/test/e2e/specs/017.already-downloaded-selfcustody-release-zip-click-download-again.spec.mts b/test/e2e/specs/017.already-downloaded-selfcustody-release-zip-click-download-again.spec.mts
deleted file mode 100644
index 22b382f5..00000000
--- a/test/e2e/specs/017.already-downloaded-selfcustody-release-zip-click-download-again.spec.mts
+++ /dev/null
@@ -1,92 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectVersion page (already downloaded release - click download again button)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await instance.selectVersionPage.waitForExist()
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await instance.selectVersionOdudexButton.waitForExist()
-    await instance.selectVersionOdudexText.waitForExist()
-    await instance.selectVersionSelfcustodyButton.click()
-    await instance.selectVersionPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipMsg.waitForExist()
-    await instance.foundReleaseZipMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningAlreadyDownloadedText.waitForExist() 
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-  })
-
-  it ('should click \'Download again\' go out of WarningDownload page', async () => {
-    await instance.warningDownloadAgainButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await expect(instance.warningDownloadPage).not.toBeDisplayed()
-  })
-
-  it ('should be in DownloadOfficialRelease page', async () => {
-    await instance.downloadOfficialReleaseZipPage.waitForExist()
-    await expect(instance.downloadOfficialReleaseZipPage).toBeDisplayed()
-  })
-
-  it('should DownloadOfficialReleaseZip page have \'Downloading\' title', async () => {
-    await instance.downloadOfficialReleaseZipTitle.waitForExist()
-    await expect(instance.downloadOfficialReleaseZipTitle).toBeDisplayed()
-    await expect(instance.downloadOfficialReleaseZipTitle).toHaveText('Downloading')
-  })
-
-  it('should DownloadOfficialReleaseZip page have \'https://github.com/selfcustody/krux/releases/download/v24.03.0/krux-v24.03.0.zip\' subtitle', async () => {
-    await instance.downloadOfficialReleaseZipTitle.waitForExist()
-    await expect(instance.downloadOfficialReleaseZipSubtitle).toBeDisplayed()
-    await expect(instance.downloadOfficialReleaseZipSubtitle).toHaveText('https://github.com/selfcustody/krux/releases/download/v24.03.0/krux-v24.03.0.zip')
-  })
-
-  it('should DownloadOfficialReleaseZip page progress until 100%', async () => {
-    await instance.downloadOfficialReleaseZipProgress.waitForExist()
-    await expect(instance.downloadOfficialReleaseZipProgress).toBeDisplayed()
-    await instance.downloadOfficialReleaseZipProgress.waitUntil(async function () {
-      const percentText = await this.getText()
-      const percent = parseFloat(percentText.split('%')[0])
-      return percent === 100.00
-    }, {
-      timeout: 600000,
-      interval: 50
-    })
-  })
-
-})
diff --git a/test/e2e/specs/018.already-downloaded-selfcustody-release-zip-click-proceed-button.spec.mts b/test/e2e/specs/018.already-downloaded-selfcustody-release-zip-click-proceed-button.spec.mts
deleted file mode 100644
index a3452561..00000000
--- a/test/e2e/specs/018.already-downloaded-selfcustody-release-zip-click-proceed-button.spec.mts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectVersion page (already downloaded release - click proceed button)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await instance.selectVersionPage.waitForExist()
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await instance.selectVersionOdudexButton.waitForExist()
-    await instance.selectVersionOdudexText.waitForExist()
-    await instance.selectVersionSelfcustodyButton.click()
-    await instance.selectVersionPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipMsg.waitForExist()
-    await instance.foundReleaseZipMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningAlreadyDownloadedText.waitForExist() 
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-  })
-
-  it ('should click \'Proceed\' button and go out of WarningDownload page', async () => {
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await expect(instance.warningDownloadPage).not.toBeDisplayed()
-  })
-  
-})
diff --git a/test/e2e/specs/019.select-version-selfcustody-release-zip-sha256.spec.mts b/test/e2e/specs/019.select-version-selfcustody-release-zip-sha256.spec.mts
deleted file mode 100644
index ab54f52e..00000000
--- a/test/e2e/specs/019.select-version-selfcustody-release-zip-sha256.spec.mts
+++ /dev/null
@@ -1,99 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-
-describe('KruxInstaller SelectVersion page (download release sha256.txt)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await instance.selectVersionPage.waitForExist()
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await instance.selectVersionOdudexButton.waitForExist()
-    await instance.selectVersionOdudexText.waitForExist()
-    await instance.selectVersionSelfcustodyButton.click()
-    await instance.selectVersionPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipMsg.waitForExist()
-    await instance.foundReleaseZipMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningAlreadyDownloadedText.waitForExist() 
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-  })
-
-  it ('should \'checking v24.03.0/krux-v24.03.0.zip.sha256.txt\' message appears', async () => {
-    await instance.checkingReleaseZipSha256txtMsg.waitForExist()
-    await expect(instance.checkingReleaseZipSha256txtMsg).toBeDisplayed()
-  })
-
-  it ('should \'v24.03.0/krux-v24.03.0.zip.sha256.txt not found\' message appears', async () => {
-    await instance.notFoundReleaseZipSha256txtMsg.waitForExist()
-    await expect(instance.notFoundReleaseZipSha256txtMsg).toBeDisplayed()
-  })
-
-  it('should go to DownloadOfficialReleaseZipSha256 page', async () => {
-    await instance.downloadOfficialReleaseZipSha256txtPage.waitForExist()
-    await expect(instance.downloadOfficialReleaseZipSha256txtPage).toBeDisplayed()
-  })
-
-  it('should DownloadOfficialReleaseZipSha256 page have \'Downloading\' title', async () => {
-    await instance.downloadOfficialReleaseZipSha256txtPageTitle.waitForExist()
-    await expect(instance.downloadOfficialReleaseZipSha256txtPageTitle).toBeDisplayed()
-    await expect(instance.downloadOfficialReleaseZipSha256txtPageTitle).toHaveText('Downloading')
-  })
-
-  it('should DownloadOfficialReleaseZipSha256 page have \'https://github.com/selfcustody/krux/releases/download/v24.03.0/krux-v24.03.0.zip.sha256.txt\' subtitle', async () => {
-    await instance.downloadOfficialReleaseZipSha256txtPageSubtitle.waitForExist()
-    await expect(instance.downloadOfficialReleaseZipSha256txtPageSubtitle).toBeDisplayed()
-    await expect(instance.downloadOfficialReleaseZipSha256txtPageSubtitle).toHaveText('https://github.com/selfcustody/krux/releases/download/v24.03.0/krux-v24.03.0.zip.sha256.txt')
-  })
-
-  it('should DownloadOfficialReleaseZipSha256 page progress until 100%', async () => {
-    await instance.downloadOfficialReleaseZipSha256txtPageProgress.waitForExist()
-    await expect(instance.downloadOfficialReleaseZipSha256txtPageProgress).toBeDisplayed()
-    await instance.downloadOfficialReleaseZipSha256txtPageProgress.waitUntil(async function () {
-      const percentText = await this.getText()
-      const percent = parseFloat(percentText.split('%')[0])
-      return percent === 100.00
-    }, {
-      timeout: 60000,
-      interval: 50
-    })
-  })
-  
-})
diff --git a/test/e2e/specs/020.already-downloaded-selfcustody-release-zip-sha256.spec.mts b/test/e2e/specs/020.already-downloaded-selfcustody-release-zip-sha256.spec.mts
deleted file mode 100644
index 8c6950fc..00000000
--- a/test/e2e/specs/020.already-downloaded-selfcustody-release-zip-sha256.spec.mts
+++ /dev/null
@@ -1,114 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectVersion page (already downloaded release sha256.txt - show only)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await instance.selectVersionPage.waitForExist()
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await instance.selectVersionOdudexButton.waitForExist()
-    await instance.selectVersionOdudexText.waitForExist()
-    await instance.selectVersionSelfcustodyButton.click()
-    await instance.selectVersionPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipMsg.waitForExist()
-    await instance.foundReleaseZipMsg.waitForExist()
-    await instance.warningAlreadyDownloadedText.waitForExist() 
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-  })
-
-  it ('should \'v24.03.0/krux-v24.03.0.zip.sha256.txt found\' message appears', async () => {
-    await instance.checkingReleaseZipSha256txtMsg.waitForExist()
-    await expect(instance.checkingReleaseZipSha256txtMsg).toBeDisplayed()
-    if (process.platform === 'linux' || process.platform === 'darwin') {
-      await expect(instance.foundReleaseZipSha256txtMsg).toHaveText('v24.03.0/krux-v24.03.0.zip.sha256.txt found')
-    } else if (process.platform === 'win32') {
-      await expect(instance.foundReleaseZipSha256txtMsg).toHaveText('v24.03.0\\krux-v24.03.0.zip.sha256.txt found')
-    }
-  })
-
-  it('should WarningDownload page should be displayed', async () => {
-    await instance.warningDownloadPage.waitForExist()
-    await expect(instance.warningDownloadPage).toBeDisplayed()
-  }) 
-
-  it('should \'v24.03.0/krux-v24.03.0.zip.sha256.txt already downloaded\' message be displayed', async () => {
-    await instance.warningAlreadyDownloadedText.waitForExist()
-    await expect(instance.warningAlreadyDownloadedText).toBeDisplayed()
-    if (process.platform === 'linux' || process.platform === 'darwin') {
-      await expect(instance.warningAlreadyDownloadedText).toHaveText('v24.03.0/krux-v24.03.0.zip.sha256.txt already downloaded')
-    } else if (process.platform === 'win32') {
-      await expect(instance.warningAlreadyDownloadedText).toHaveText('v24.03.0\\krux-v24.03.0.zip.sha256.txt already downloaded')
-    }
-  })
-
-  it('should \'Proceed with current file\' button be displayed', async () => {
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await expect(instance.warningDownloadProceedButton).toBeDisplayed()
-    await expect(instance.warningDownloadProceedButtonText).toBeDisplayed()
-    await expect(instance.warningDownloadProceedButtonText).toHaveText('Proceed with current file')
-  })
-
-  it('should \'Download it again\' button be displayed', async () => {
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await expect(instance.warningDownloadAgainButton).toBeDisplayed()
-    await expect(instance.warningDownloadAgainButtonText).toBeDisplayed()
-    await expect(instance.warningDownloadAgainButtonText).toHaveText('Download it again')
-  })
-
-  it('should \'Show details\' button be displayed', async () => {
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await expect(instance.warningDownloadShowDetailsButton).toBeDisplayed()
-    await expect(instance.warningDownloadShowDetailsButtonText).toBeDisplayed()
-    await expect(instance.warningDownloadShowDetailsButtonText).toHaveText('Show details')
-  })
-
-  it('should \'Back\' button be displayed', async () => {
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-    await expect(instance.warningDownloadBackButton).toBeDisplayed()
-    await expect(instance.warningDownloadBackButtonText).toBeDisplayed()
-    await expect(instance.warningDownloadBackButtonText).toHaveText('Back')
-  })
-
-})
diff --git a/test/e2e/specs/021.already-downloaded-selfcustody-release-zip-sha256-click-back-button.spec.mts b/test/e2e/specs/021.already-downloaded-selfcustody-release-zip-sha256-click-back-button.spec.mts
deleted file mode 100644
index 8296f1ac..00000000
--- a/test/e2e/specs/021.already-downloaded-selfcustody-release-zip-sha256-click-back-button.spec.mts
+++ /dev/null
@@ -1,79 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectVersion page (already downloaded release sha256.txt - click back button)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await instance.selectVersionPage.waitForExist()
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await instance.selectVersionOdudexButton.waitForExist()
-    await instance.selectVersionOdudexText.waitForExist()
-    await instance.selectVersionSelfcustodyButton.click()
-    await instance.selectVersionPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipMsg.waitForExist()
-    await instance.foundReleaseZipMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningAlreadyDownloadedText.waitForExist() 
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSha256txtMsg.waitForExist()
-    await instance.foundReleaseZipSha256txtMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadBackButton.waitForExist()
-  })
-
-  it ('should click \'Back\' button and go out from WarningDownload page', async () => {
-    await instance.warningDownloadBackButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await expect(instance.warningDownloadPage).not.toBeDisplayed()
-  })
-
-  it('should Main Page be displayed', async () => {
-    await instance.mainPage.waitForExist()
-    await expect(instance.mainPage).toBeDisplayed()
-  }) 
-
-  it('should \'Select version\' button changed to \'Version: selfcustody/krux/releases/tag/v24.03.0\'', async () => {
-    await instance.mainSelectVersionText.waitForExist()
-    await expect(instance.mainSelectVersionText).toBeDisplayed()
-    await expect(instance.mainSelectVersionText).toHaveText('Version: selfcustody/krux/releases/tag/v24.03.0')
-  })
-
-})
diff --git a/test/e2e/specs/022.already-downloaded-selfcustody-release-zip-sha256-click-show-details-button.spec.mts b/test/e2e/specs/022.already-downloaded-selfcustody-release-zip-sha256-click-show-details-button.spec.mts
deleted file mode 100644
index 84860547..00000000
--- a/test/e2e/specs/022.already-downloaded-selfcustody-release-zip-sha256-click-show-details-button.spec.mts
+++ /dev/null
@@ -1,140 +0,0 @@
-import { expect } from '@wdio/globals'
-import { join } from 'path'
-import { homedir } from 'os'
-import { osLangSync } from 'os-lang'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectVersion page (already downloaded release sha256.txt - click show details button)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await instance.selectVersionPage.waitForExist()
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await instance.selectVersionOdudexButton.waitForExist()
-    await instance.selectVersionOdudexText.waitForExist()
-    await instance.selectVersionSelfcustodyButton.click()
-    await instance.selectVersionPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipMsg.waitForExist()
-    await instance.foundReleaseZipMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningAlreadyDownloadedText.waitForExist() 
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSha256txtMsg.waitForExist()
-    await instance.foundReleaseZipSha256txtMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-  })
-
-  it ('should overlay not be shown', async () => {
-    await expect(instance.warningAlreadyDownloadedOverlay).not.toBeDisplayed()
-  })
-
-  it ('should click \'Show details\' button and overlay must be visible', async () => {
-    await instance.warningDownloadShowDetailsButton.click()
-    await instance.warningAlreadyDownloadedOverlay.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlay).toBeDisplayed()
-  })
-
-  it('should overlay title be \'Resource details\'', async () => {
-    await instance.warningAlreadyDownloadedOverlayTitle.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlayTitle).toBeDisplayed()
-    await expect(instance.warningAlreadyDownloadedOverlayTitle).toHaveText('Resource details')
-  })
-
-  it ('should overlay subtitle be \'v24.03.0/krux-v24.03.0.zip.sha256.txt\'', async () => {
-    await instance.warningAlreadyDownloadedOverlayTitle.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlaySubtitle).toBeDisplayed()
-    await expect(instance.warningAlreadyDownloadedOverlaySubtitle).toHaveText('v24.03.0/krux-v24.03.0.zip.sha256.txt')
-  })
-
-  it ('should a overlay text have \'Remote: https://github.com/selfcustody/krux/releases/download/v24.03.0/krux-v24.03.0.zip.sha256.txt\'', async () => {
-    await instance.warningAlreadyDownloadedOverlayTextRemote.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlayTextRemote).toBeDisplayed()
-    await expect(instance.warningAlreadyDownloadedOverlayTextRemote).toHaveText('Remote:\nhttps://github.com/selfcustody/krux/releases/download/v24.03.0/krux-v24.03.0.zip.sha256.txt')
-  })
-
-  it ('should a overlay text have properly local resource', async () => {
-    await instance.warningAlreadyDownloadedOverlayTextLocal.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlayTextLocal).toBeDisplayed()
-    
-    let resources = ''
-    if (process.env.CI && process.env.GITHUB_ACTION) {
-      if (process.platform  === 'linux') {
-        resources = '/home/runner/krux-installer'
-      } else if (process.platform  === 'win32') {
-        resources = 'C:\\Users\\runneradmin\\Documents\\krux-installer'
-      } else if (process.platform  === 'darwin') {
-        resources = '/Users/runner/Documents/krux-installer'
-      }
-    } else {
-      const lang = osLangSync()
-      const home = homedir()
-      if ( lang.match(/en-*/g)) {
-        resources = join(home, 'Documents', 'krux-installer')
-      } else if ( lang.match(/pt-*/g)) {
-        resources = join(home, 'Documentos', 'krux-installer')
-      } else {
-        throw new Error(`${lang} not implemented. Please implement it with correct \'Documents\' folder name`)
-      }
-    }
-
-    const resource = join(resources, 'v24.03.0', 'krux-v24.03.0.zip.sha256.txt')
-    await expect(instance.warningAlreadyDownloadedOverlayTextLocal).toHaveText(`Local:\n${resource}`)
-  })
-
-  it('should a overlay text have the properly description', async () => {
-    await instance.warningAlreadyDownloadedOverlayTextWhatdo.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlayTextWhatdo).toBeDisplayed()
-    await expect(instance.warningAlreadyDownloadedOverlayTextWhatdo).toHaveText('Description:\nThis file proves the integrity of previous file. It uses the sha256 algorithm to check if zip file has not be changed during download.')
-  })
-
-  it('should \'close\' have \'Close\' text',async () => {
-    await instance.warningAlreadyDownloadedOverlayButtonClose.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlayButtonClose).toBeDisplayed()
-    await expect(instance.warningAlreadyDownloadedOverlayButtonClose).toHaveText('Close')
-  })
-
-  it('should \'close\' button make overlay not visible', async () => {
-    await instance.warningAlreadyDownloadedOverlayButtonClose.click()
-    await instance.warningAlreadyDownloadedOverlay.waitForExist({ reverse: true })
-    await expect(instance.warningAlreadyDownloadedOverlay).not.toBeDisplayed()
-  })
-  
-})
diff --git a/test/e2e/specs/023.already-downloaded-selfcustody-release-zip-sha256-click-download-again-button.spec.mts b/test/e2e/specs/023.already-downloaded-selfcustody-release-zip-sha256-click-download-again-button.spec.mts
deleted file mode 100644
index 41283836..00000000
--- a/test/e2e/specs/023.already-downloaded-selfcustody-release-zip-sha256-click-download-again-button.spec.mts
+++ /dev/null
@@ -1,99 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectVersion page (already downloaded release sha256.txt - click download again button)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await instance.selectVersionPage.waitForExist()
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await instance.selectVersionOdudexButton.waitForExist()
-    await instance.selectVersionOdudexText.waitForExist()
-    await instance.selectVersionSelfcustodyButton.click()
-    await instance.selectVersionPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipMsg.waitForExist()
-    await instance.foundReleaseZipMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningAlreadyDownloadedText.waitForExist() 
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSha256txtMsg.waitForExist()
-    await instance.foundReleaseZipSha256txtMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-  })
-
-  it ('should click \'Download again\' go out of WarningDownload page', async () => {
-    await instance.warningDownloadAgainButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await expect(instance.warningDownloadPage).not.toBeDisplayed()
-  })
-
-  it ('should be in DownloadOfficialReleaseZipSha256 page', async () => {
-    await instance.downloadOfficialReleaseZipSha256txtPage.waitForExist()
-    await expect(instance.downloadOfficialReleaseZipSha256txtPage).toBeDisplayed()
-  })
-
-  it('should DownloadOfficialReleaseZipSha256 page have \'Downloading\' title', async () => {
-    await instance.downloadOfficialReleaseZipSha256txtPageTitle.waitForExist()
-    await expect(instance.downloadOfficialReleaseZipSha256txtPageTitle).toBeDisplayed()
-    await expect(instance.downloadOfficialReleaseZipSha256txtPageTitle).toHaveText('Downloading')
-  })
-
-  it('should DownloadOfficialReleaseZipSha256 page have \'https://github.com/selfcustody/krux/releases/download/v24.03.0/krux-v24.03.0.zip.sha256.txt\' subtitle', async () => {
-    await instance.downloadOfficialReleaseZipSha256txtPageSubtitle.waitForExist()
-    await expect(instance.downloadOfficialReleaseZipSha256txtPageSubtitle).toBeDisplayed()
-    await expect(instance.downloadOfficialReleaseZipSha256txtPageSubtitle).toHaveText('https://github.com/selfcustody/krux/releases/download/v24.03.0/krux-v24.03.0.zip.sha256.txt')
-  })
-
-  it('should DownloadOfficialReleaseZipSha256 page progress until 100%', async () => {
-    await instance.downloadOfficialReleaseZipSha256txtPageProgress.waitForExist()
-    await expect(instance.downloadOfficialReleaseZipSha256txtPageProgress).toBeDisplayed()
-    await instance.downloadOfficialReleaseZipSha256txtPageProgress.waitUntil(async function () {
-      const percentText = await this.getText()
-      const percent = parseFloat(percentText.split('%')[0])
-      return percent === 100.00
-    }, {
-      timeout: 600000,
-      interval: 50
-    })
-  })
-  
-})
diff --git a/test/e2e/specs/024.already-downloaded-selfcustody-release-zip-sha256-click-proceed-button.spec.mts b/test/e2e/specs/024.already-downloaded-selfcustody-release-zip-sha256-click-proceed-button.spec.mts
deleted file mode 100644
index bb848130..00000000
--- a/test/e2e/specs/024.already-downloaded-selfcustody-release-zip-sha256-click-proceed-button.spec.mts
+++ /dev/null
@@ -1,69 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectVersion page (already downloaded release sha256.txt - click  proceed button)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await instance.selectVersionPage.waitForExist()
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await instance.selectVersionOdudexButton.waitForExist()
-    await instance.selectVersionOdudexText.waitForExist()
-    await instance.selectVersionSelfcustodyButton.click()
-    await instance.selectVersionPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipMsg.waitForExist()
-    await instance.foundReleaseZipMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningAlreadyDownloadedText.waitForExist() 
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSha256txtMsg.waitForExist()
-    await instance.foundReleaseZipSha256txtMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-  })
-
-  it ('should click \'Proceed\' go out of WarningDownload page', async () => {
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await expect(instance.warningDownloadPage).not.toBeDisplayed()
-  })
-  
-})
diff --git a/test/e2e/specs/025.select-version-selfcustody-release-zip-sig.spec.mts b/test/e2e/specs/025.select-version-selfcustody-release-zip-sig.spec.mts
deleted file mode 100644
index ae3d7a32..00000000
--- a/test/e2e/specs/025.select-version-selfcustody-release-zip-sig.spec.mts
+++ /dev/null
@@ -1,105 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectVersion page (download release signature)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await instance.selectVersionPage.waitForExist()
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await instance.selectVersionOdudexButton.waitForExist()
-    await instance.selectVersionOdudexText.waitForExist()
-    await instance.selectVersionSelfcustodyButton.click()
-    await instance.selectVersionPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipMsg.waitForExist()
-    await instance.foundReleaseZipMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningAlreadyDownloadedText.waitForExist() 
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSha256txtMsg.waitForExist()
-    await instance.foundReleaseZipSha256txtMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-  })
-
-  it ('should \'checking v24.03.0/krux-v24.03.0.zip.sig\' message appears', async () => {
-    await instance.checkingReleaseZipSigMsg.waitForExist()
-    await expect(instance.checkingReleaseZipSigMsg).toBeDisplayed()
-  })
-
-  it ('should \'v24.03.0/krux-v24.03.0.zip.sig not found\' message appears', async () => {
-    await instance.notFoundReleaseZipSigMsg.waitForExist()
-    await expect(instance.notFoundReleaseZipSigMsg).toBeDisplayed()
-  })
-
-  it('should go to DownloadOfficialReleaseZipSig page', async () => {
-    await instance.downloadOfficialReleaseZipSigPage.waitForExist()
-    await expect(instance.downloadOfficialReleaseZipSigPage).toBeDisplayed()
-  })
-
-  it('should DownloadOfficialReleaseZipSig page have \'Downloading\' title', async () => {
-    await instance.downloadOfficialReleaseZipSigTitle.waitForExist()
-    await expect(instance.downloadOfficialReleaseZipSigTitle).toBeDisplayed()
-    await expect(instance.downloadOfficialReleaseZipSigTitle).toHaveText('Downloading')
-  })
-
-  it('should DownloadOfficialReleaseZipSig page have \'https://github.com/selfcustody/krux/releases/download/v24.03.0/krux-v24.03.0.zip.sig\' subtitle', async () => {
-    await instance.downloadOfficialReleaseZipSigSubtitle.waitForExist()
-    await expect(instance.downloadOfficialReleaseZipSigSubtitle).toBeDisplayed()
-    await expect(instance.downloadOfficialReleaseZipSigSubtitle).toHaveText('https://github.com/selfcustody/krux/releases/download/v24.03.0/krux-v24.03.0.zip.sig')
-  })
-
-  it('should DownloadOfficialReleaseZipSig page progress until 100%', async () => {
-    await instance.downloadOfficialReleaseZipSigProgress.waitForExist()
-    await expect(instance.downloadOfficialReleaseZipSigProgress).toBeDisplayed()
-    await instance.downloadOfficialReleaseZipSigProgress.waitUntil(async function () {
-      const percentText = await this.getText()
-      const percent = parseFloat(percentText.split('%')[0])
-      return percent === 100.00
-    }, {
-      timeout: 60000,
-      interval: 50
-    })
-  })
-  
-})
diff --git a/test/e2e/specs/026.already-downloaded-selfcustody-release-zip-sig.spec.mts b/test/e2e/specs/026.already-downloaded-selfcustody-release-zip-sig.spec.mts
deleted file mode 100644
index c8008cf8..00000000
--- a/test/e2e/specs/026.already-downloaded-selfcustody-release-zip-sig.spec.mts
+++ /dev/null
@@ -1,122 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectVersion page (already downloaded  release signature - show only)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await instance.selectVersionPage.waitForExist()
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await instance.selectVersionOdudexButton.waitForExist()
-    await instance.selectVersionOdudexText.waitForExist()
-    await instance.selectVersionSelfcustodyButton.click()
-    await instance.selectVersionPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipMsg.waitForExist()
-    await instance.foundReleaseZipMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningAlreadyDownloadedText.waitForExist() 
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSha256txtMsg.waitForExist()
-    await instance.foundReleaseZipSha256txtMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-  })
-
-  it ('should \'v24.03.0/krux-v24.03.0.zip.sig found\' message appears', async () => {
-    await instance.checkingReleaseZipSigMsg.waitForExist()
-    await expect(instance.foundReleaseZipSigMsg).toBeDisplayed()
-    if (process.platform === 'linux' || process.platform === 'darwin') {
-      await expect(instance.foundReleaseZipSigMsg).toHaveText('v24.03.0/krux-v24.03.0.zip.sig found')
-    } else if (process.platform === 'win32') {
-      await expect(instance.foundReleaseZipSigMsg).toHaveText('v24.03.0\\krux-v24.03.0.zip.sig found')
-    }
-  })
-
-  it('should WarningDownload page should be displayed', async () => {
-    await instance.warningDownloadPage.waitForExist()
-    await expect(instance.warningDownloadPage).toBeDisplayed()
-  }) 
-
-  it('should \'v24.03.0/krux-v24.03.0.zip.sig already downloaded\' message be displayed', async () => {
-    await instance.warningAlreadyDownloadedText.waitForExist()
-    await expect(instance.warningAlreadyDownloadedText).toBeDisplayed()
-    if (process.platform === 'linux' || process.platform === 'darwin') {
-      await expect(instance.warningAlreadyDownloadedText).toHaveText('v24.03.0/krux-v24.03.0.zip.sig already downloaded')
-    } else if (process.platform === 'win32') {
-      await expect(instance.warningAlreadyDownloadedText).toHaveText('v24.03.0\\krux-v24.03.0.zip.sig already downloaded')
-    }
-  })
-
-  it('should \'Proceed with current file\' button be displayed', async () => {
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await expect(instance.warningDownloadProceedButton).toBeDisplayed()
-    await expect(instance.warningDownloadProceedButtonText).toBeDisplayed()
-    await expect(instance.warningDownloadProceedButtonText).toHaveText('Proceed with current file')
-  })
-
-  it('should \'Download it again\' button be displayed', async () => {
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await expect(instance.warningDownloadAgainButton).toBeDisplayed()
-    await expect(instance.warningDownloadAgainButtonText).toBeDisplayed()
-    await expect(instance.warningDownloadAgainButtonText).toHaveText('Download it again')
-  })
-
-  it('should \'Show details\' button be displayed', async () => {
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await expect(instance.warningDownloadShowDetailsButton).toBeDisplayed()
-    await expect(instance.warningDownloadShowDetailsButtonText).toBeDisplayed()
-    await expect(instance.warningDownloadShowDetailsButtonText).toHaveText('Show details')
-  })
-
-  it('should \'Back\' button be displayed', async () => {
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-    await expect(instance.warningDownloadBackButton).toBeDisplayed()
-    await expect(instance.warningDownloadBackButtonText).toBeDisplayed()
-    await expect(instance.warningDownloadBackButtonText).toHaveText('Back')
-  })
-  
-})
diff --git a/test/e2e/specs/027.already-downloaded-selfcustody-release-zip-sig-click-back-button.spec.mts b/test/e2e/specs/027.already-downloaded-selfcustody-release-zip-sig-click-back-button.spec.mts
deleted file mode 100644
index 77c9db50..00000000
--- a/test/e2e/specs/027.already-downloaded-selfcustody-release-zip-sig-click-back-button.spec.mts
+++ /dev/null
@@ -1,86 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectVersion page (already downloaded  release signature - click back button)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await instance.selectVersionPage.waitForExist()
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await instance.selectVersionOdudexButton.waitForExist()
-    await instance.selectVersionOdudexText.waitForExist()
-    await instance.selectVersionSelfcustodyButton.click()
-    await instance.selectVersionPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipMsg.waitForExist()
-    await instance.foundReleaseZipMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningAlreadyDownloadedText.waitForExist() 
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSha256txtMsg.waitForExist()
-    await instance.foundReleaseZipSha256txtMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSigMsg.waitForExist()
-    await instance.foundReleaseZipSigMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadBackButton.waitForExist()
-  })
-
-  it ('should click \'Back\' button and go out from WarningDownload page', async () => {
-    await instance.warningDownloadBackButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await expect(instance.warningDownloadPage).not.toBeDisplayed()
-  })
-
-  it('should Main Page be displayed', async () => {
-    await instance.mainPage.waitForExist()
-    await expect(instance.mainPage).toBeDisplayed()
-  }) 
-
-  it('should \'Select version\' button changed to \'Version: selfcustody/krux/releases/tag/v24.03.0\'', async () => {
-    await instance.mainSelectVersionText.waitForExist()
-    await expect(instance.mainSelectVersionText).toBeDisplayed()
-    await expect(instance.mainSelectVersionText).toHaveText('Version: selfcustody/krux/releases/tag/v24.03.0')
-  })
-  
-})
diff --git a/test/e2e/specs/028.already-downloaded-selfcustody-release-zip-sig-show-details-button.spec.mts b/test/e2e/specs/028.already-downloaded-selfcustody-release-zip-sig-show-details-button.spec.mts
deleted file mode 100644
index 517c4287..00000000
--- a/test/e2e/specs/028.already-downloaded-selfcustody-release-zip-sig-show-details-button.spec.mts
+++ /dev/null
@@ -1,147 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { join } from 'path'
-import { homedir } from 'os'
-import { osLangSync } from 'os-lang'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectVersion page (already downloaded release signature - click show details button)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await instance.selectVersionPage.waitForExist()
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await instance.selectVersionOdudexButton.waitForExist()
-    await instance.selectVersionOdudexText.waitForExist()
-    await instance.selectVersionSelfcustodyButton.click()
-    await instance.selectVersionPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipMsg.waitForExist()
-    await instance.foundReleaseZipMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningAlreadyDownloadedText.waitForExist() 
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSha256txtMsg.waitForExist()
-    await instance.foundReleaseZipSha256txtMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSigMsg.waitForExist()
-    await instance.foundReleaseZipSigMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-  })
-
-  it ('should overlay not be shown', async () => {
-    await expect(instance.warningAlreadyDownloadedOverlay).not.toBeDisplayed()
-  })
-
-  it ('should click \'Show details\' button and overlay must be visible', async () => {
-    await instance.warningDownloadShowDetailsButton.click()
-    await instance.warningAlreadyDownloadedOverlay.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlay).toBeDisplayed()
-  })
-
-  it('should overlay title be \'Resource details\'', async () => {
-    await instance.warningAlreadyDownloadedOverlayTitle.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlayTitle).toBeDisplayed()
-    await expect(instance.warningAlreadyDownloadedOverlayTitle).toHaveText('Resource details')
-  })
-
-  it ('should overlay subtitle be \'v24.03.0/krux-v24.03.0.zip.sig\'', async () => {
-    await instance.warningAlreadyDownloadedOverlayTitle.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlaySubtitle).toBeDisplayed()
-    await expect(instance.warningAlreadyDownloadedOverlaySubtitle).toHaveText('v24.03.0/krux-v24.03.0.zip.sig')
-  })
-
-  it ('should a overlay text have \'Remote: https://github.com/selfcustody/krux/releases/download/v24.03.0/krux-v24.03.0.zip.sig\'', async () => {
-    await instance.warningAlreadyDownloadedOverlayTextRemote.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlayTextRemote).toBeDisplayed()
-    await expect(instance.warningAlreadyDownloadedOverlayTextRemote).toHaveText('Remote:\nhttps://github.com/selfcustody/krux/releases/download/v24.03.0/krux-v24.03.0.zip.sig')
-  })
-
-  it ('should a overlay text have properly local resource', async () => {
-    await instance.warningAlreadyDownloadedOverlayTextLocal.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlayTextLocal).toBeDisplayed()
-    
-    let resources = ''
-    if (process.env.CI && process.env.GITHUB_ACTION) {
-      if (process.platform  === 'linux') {
-        resources = '/home/runner/krux-installer'
-      } else if (process.platform  === 'win32') {
-        resources = 'C:\\Users\\runneradmin\\Documents\\krux-installer'
-      } else if (process.platform  === 'darwin') {
-        resources = '/Users/runner/Documents/krux-installer'
-      }
-    } else {
-      const lang = osLangSync()
-      const home = homedir()
-      if (lang.match(/en-*/g)) {
-        resources = join(home, 'Documents', 'krux-installer')
-      } else if ( lang.match(/pt-*/g)) {
-        resources = join(home, 'Documentos', 'krux-installer')
-      } else {
-        throw new Error(`${lang} not implemented. Please implement it with correct \'Documents\' folder name`)
-      }
-    }
-
-    const resource = join(resources, 'v24.03.0', 'krux-v24.03.0.zip.sig')
-    await expect(instance.warningAlreadyDownloadedOverlayTextLocal).toHaveText(`Local:\n${resource}`)
-  })
-
-  it('should a overlay text have the properly description', async () => {
-    await instance.warningAlreadyDownloadedOverlayTextWhatdo.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlayTextWhatdo).toBeDisplayed()
-    await expect(instance.warningAlreadyDownloadedOverlayTextWhatdo).toHaveText('Description:\nThis file, with the public key certificate, proves the authenticity of zip file, checking if the zip file was signed by its creator.')
-  })
-
-  it('should \'close\' have \'Close\' text',async () => {
-    await instance.warningAlreadyDownloadedOverlayButtonClose.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlayButtonClose).toBeDisplayed()
-    await expect(instance.warningAlreadyDownloadedOverlayButtonClose).toHaveText('Close')
-  })
-
-  it('should \'close\' button make overlay not visible', async () => {
-    await instance.warningAlreadyDownloadedOverlayButtonClose.click()
-    await instance.warningAlreadyDownloadedOverlay.waitForExist({ reverse: true })
-    await expect(instance.warningAlreadyDownloadedOverlay).not.toBeDisplayed()
-  })
-  
-})
diff --git a/test/e2e/specs/029.already-downloaded-selfcustody-release-zip-sig-download-again-button.spec.mts b/test/e2e/specs/029.already-downloaded-selfcustody-release-zip-sig-download-again-button.spec.mts
deleted file mode 100644
index 91ed947a..00000000
--- a/test/e2e/specs/029.already-downloaded-selfcustody-release-zip-sig-download-again-button.spec.mts
+++ /dev/null
@@ -1,106 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectVersion page (already downloaded  release signature - click download again button)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await instance.selectVersionPage.waitForExist()
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await instance.selectVersionOdudexButton.waitForExist()
-    await instance.selectVersionOdudexText.waitForExist()
-    await instance.selectVersionSelfcustodyButton.click()
-    await instance.selectVersionPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipMsg.waitForExist()
-    await instance.foundReleaseZipMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningAlreadyDownloadedText.waitForExist() 
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSha256txtMsg.waitForExist()
-    await instance.foundReleaseZipSha256txtMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSigMsg.waitForExist()
-    await instance.foundReleaseZipSigMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-  })
-
-  it ('should click \'Download again\' go out of WarningDownload page', async () => {
-    await instance.warningDownloadAgainButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await expect(instance.warningDownloadPage).not.toBeDisplayed()
-  })
-
-  it ('should be in DownloadOfficialReleaseSig page', async () => {
-    await instance.downloadOfficialReleaseZipSigPage.waitForExist()
-    await expect(instance.downloadOfficialReleaseZipSigPage).toBeDisplayed()
-  })
-
-  it('should DownloadOfficialReleaseSig page have \'Downloading\' title', async () => {
-    await instance.downloadOfficialReleaseZipSigTitle.waitForExist()
-    await expect(instance.downloadOfficialReleaseZipSigTitle).toBeDisplayed()
-    await expect(instance.downloadOfficialReleaseZipSigTitle).toHaveText('Downloading')
-  })
-
-  it('should DownloadOfficialReleaseSig page have \'https://github.com/selfcustody/krux/releases/download/v24.03.0/krux-v24.03.0.zip.sig\' subtitle', async () => {
-    await instance.downloadOfficialReleaseZipSigSubtitle.waitForExist()
-    await expect(instance.downloadOfficialReleaseZipSigSubtitle).toBeDisplayed()
-    await expect(instance.downloadOfficialReleaseZipSigSubtitle).toHaveText('https://github.com/selfcustody/krux/releases/download/v24.03.0/krux-v24.03.0.zip.sig')
-  })
-
-  it('should DownloadOfficialReleaseSig page progress until 100%', async () => {
-    await instance.downloadOfficialReleaseZipSigProgress.waitForExist()
-    await expect(instance.downloadOfficialReleaseZipSigProgress).toBeDisplayed()
-    await instance.downloadOfficialReleaseZipSigProgress.waitUntil(async function () {
-      const percentText = await this.getText()
-      const percent = parseFloat(percentText.split('%')[0])
-      return percent === 100.00
-    }, {
-      timeout: 600000,
-      interval: 50
-    })
-  })
-  
-})
diff --git a/test/e2e/specs/030.already-downloaded-selfcustody-release-zip-sig-click-proceed-button.spec.mts b/test/e2e/specs/030.already-downloaded-selfcustody-release-zip-sig-click-proceed-button.spec.mts
deleted file mode 100644
index 365dbca4..00000000
--- a/test/e2e/specs/030.already-downloaded-selfcustody-release-zip-sig-click-proceed-button.spec.mts
+++ /dev/null
@@ -1,76 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectVersion page (already downloaded  release signature - click proceed button)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await instance.selectVersionPage.waitForExist()
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await instance.selectVersionOdudexButton.waitForExist()
-    await instance.selectVersionOdudexText.waitForExist()
-    await instance.selectVersionSelfcustodyButton.click()
-    await instance.selectVersionPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipMsg.waitForExist()
-    await instance.foundReleaseZipMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningAlreadyDownloadedText.waitForExist() 
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSha256txtMsg.waitForExist()
-    await instance.foundReleaseZipSha256txtMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSigMsg.waitForExist()
-    await instance.foundReleaseZipSigMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-  })
-   
-  it ('should click \'Proceed\' go out of WarningDownload page', async () => {
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await expect(instance.warningDownloadPage).not.toBeDisplayed()
-  })
-     
-})
diff --git a/test/e2e/specs/031-select-version-selfcustody-pem.spec.mts b/test/e2e/specs/031-select-version-selfcustody-pem.spec.mts
deleted file mode 100644
index 9883891a..00000000
--- a/test/e2e/specs/031-select-version-selfcustody-pem.spec.mts
+++ /dev/null
@@ -1,112 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectVersion page (download public key certificate)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await instance.selectVersionPage.waitForExist()
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await instance.selectVersionOdudexButton.waitForExist()
-    await instance.selectVersionOdudexText.waitForExist()
-    await instance.selectVersionSelfcustodyButton.click()
-    await instance.selectVersionPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipMsg.waitForExist()
-    await instance.foundReleaseZipMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningAlreadyDownloadedText.waitForExist() 
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSha256txtMsg.waitForExist()
-    await instance.foundReleaseZipSha256txtMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSigMsg.waitForExist()
-    await instance.foundReleaseZipSigMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-  })
-   
-  it ('should \'checking selfcustody.pem\' message appears', async () => {
-    await instance.checkingReleasePemMsg.waitForExist()
-    await expect(instance.checkingReleasePemMsg).toBeDisplayed()
-  })
-
-  it ('should \'main/selfcustody.pem not found\' message appears', async () => {
-    await instance.notFoundReleasePemMsg.waitForExist()
-    await expect(instance.notFoundReleasePemMsg).toBeDisplayed()
-  })
-
-  it('should go to DownloadOfficialReleasePem page', async () => {
-    await instance.downloadOfficialReleasePemPage.waitForExist()
-    await expect(instance.downloadOfficialReleasePemPage).toBeDisplayed()
-  })
-
-  it('should DownloadOfficialReleasePem page have \'Downloading\' title', async () => {
-    await instance.downloadOfficialReleasePemTitle.waitForExist()
-    await expect(instance.downloadOfficialReleasePemTitle).toBeDisplayed()
-    await expect(instance.downloadOfficialReleasePemTitle).toHaveText('Downloading')
-  })
-
-  it('should DownloadOfficialReleasePem page have \'https://raw.githubusercontent.com/selfcustody/krux/main/selfcustody.pem\' subtitle', async () => {
-    await instance.downloadOfficialReleasePemSubtitle.waitForExist()
-    await expect(instance.downloadOfficialReleasePemSubtitle).toBeDisplayed()
-    await expect(instance.downloadOfficialReleasePemSubtitle).toHaveText('https://raw.githubusercontent.com/selfcustody/krux/main/selfcustody.pem')
-  })
-
-  it('should DownloadOfficialReleasePem page progress until 100%', async () => {
-    await instance.downloadOfficialReleasePemProgress.waitForExist()
-    await expect(instance.downloadOfficialReleasePemProgress).toBeDisplayed()
-    // TODO: Pem is so small that wdio cannot check progress 
-    /*await instance.downloadOfficialReleasePemProgress.waitUntil(async function () {
-      const percentText = await this.getText()
-      const percent = parseFloat(percentText.split('%')[0])
-      return percent === 100.00
-    }, {
-      timeout: 60000,
-      interval: 50
-    })*/
-  })
-
-})
diff --git a/test/e2e/specs/032.already-downloaded-selfcustody-pem.spec.mts b/test/e2e/specs/032.already-downloaded-selfcustody-pem.spec.mts
deleted file mode 100644
index 6bfe8f35..00000000
--- a/test/e2e/specs/032.already-downloaded-selfcustody-pem.spec.mts
+++ /dev/null
@@ -1,127 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectVersion page (already downloaded public key certificate - show only)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await instance.selectVersionPage.waitForExist()
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await instance.selectVersionOdudexButton.waitForExist()
-    await instance.selectVersionOdudexText.waitForExist()
-    await instance.selectVersionSelfcustodyButton.click()
-    await instance.selectVersionPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipMsg.waitForExist()
-    await instance.foundReleaseZipMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningAlreadyDownloadedText.waitForExist() 
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSha256txtMsg.waitForExist()
-    await instance.foundReleaseZipSha256txtMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSigMsg.waitForExist()
-    await instance.foundReleaseZipSigMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-  })
-   
-  it ('should \'main/selfcustody.pem found\' message appears', async () => {
-    await instance.checkingReleasePemMsg.waitForExist()
-    await expect(instance.checkingReleasePemMsg).toBeDisplayed()
-    if (process.platform === 'linux' || process.platform === 'darwin') {
-      await expect(instance.foundReleasePemMsg).toHaveText('main/selfcustody.pem found')
-    } else if (process.platform === 'win32') {
-      await expect(instance.foundReleasePemMsg).toHaveText('main\\selfcustody.pem found')
-    }
-  })
-  
-  it('should WarningDownload page should be displayed', async () => {
-    await instance.warningDownloadPage.waitForExist()
-    await expect(instance.warningDownloadPage).toBeDisplayed()
-  }) 
-
-  it('should \'main/selfcustody.pem already downloaded\' message be displayed', async () => {
-    await instance.warningAlreadyDownloadedText.waitForExist()
-    await expect(instance.warningAlreadyDownloadedText).toBeDisplayed()
-    if (process.platform === 'linux' || process.platform === 'darwin') {
-      await expect(instance.warningAlreadyDownloadedText).toHaveText('main/selfcustody.pem already downloaded')
-    } else if (process.platform === 'win32') {
-      await expect(instance.warningAlreadyDownloadedText).toHaveText('main\\selfcustody.pem already downloaded')
-    }
-  })
-
-  it('should \'Proceed with current file\' button be displayed', async () => {
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await expect(instance.warningDownloadProceedButton).toBeDisplayed()
-    await expect(instance.warningDownloadProceedButtonText).toBeDisplayed()
-    await expect(instance.warningDownloadProceedButtonText).toHaveText('Proceed with current file')
-  })
-  
-  it('should \'Download it again\' button be displayed', async () => {
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await expect(instance.warningDownloadAgainButton).toBeDisplayed()
-    await expect(instance.warningDownloadAgainButtonText).toBeDisplayed()
-    await expect(instance.warningDownloadAgainButtonText).toHaveText('Download it again')
-  })
-  
-  it('should \'Show details\' button be displayed', async () => {
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await expect(instance.warningDownloadShowDetailsButton).toBeDisplayed()
-    await expect(instance.warningDownloadShowDetailsButtonText).toBeDisplayed()
-    await expect(instance.warningDownloadShowDetailsButtonText).toHaveText('Show details')
-  })
-  
-  it('should \'Back\' button be displayed', async () => {
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-    await expect(instance.warningDownloadBackButton).toBeDisplayed()
-    await expect(instance.warningDownloadBackButtonText).toBeDisplayed()
-    await expect(instance.warningDownloadBackButtonText).toHaveText('Back')
-  })
-})
diff --git a/test/e2e/specs/033.already-downloaded-selfcustody-pem-click-back-button.spec.mts b/test/e2e/specs/033.already-downloaded-selfcustody-pem-click-back-button.spec.mts
deleted file mode 100644
index 830679b8..00000000
--- a/test/e2e/specs/033.already-downloaded-selfcustody-pem-click-back-button.spec.mts
+++ /dev/null
@@ -1,91 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectVersion page (already downloaded public key certificate - click back button)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await instance.selectVersionPage.waitForExist()
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await instance.selectVersionOdudexButton.waitForExist()
-    await instance.selectVersionOdudexText.waitForExist()
-    await instance.selectVersionSelfcustodyButton.click()
-    await instance.selectVersionPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipMsg.waitForExist()
-    await instance.foundReleaseZipMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningAlreadyDownloadedText.waitForExist() 
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSha256txtMsg.waitForExist()
-    await instance.foundReleaseZipSha256txtMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSigMsg.waitForExist()
-    await instance.foundReleaseZipSigMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleasePemMsg.waitForExist()
-    await instance.foundReleasePemMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-  })
-
-  it ('should click \'Back\' button and go out from WarningDownload page', async () => {
-    await instance.warningDownloadBackButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await expect(instance.warningDownloadPage).not.toBeDisplayed()
-  })
-    
-  it('should Main Page be displayed', async () => {
-    await instance.mainPage.waitForExist()
-    await expect(instance.mainPage).toBeDisplayed()
-  })
-   
-  it('should \'Select version\' button changed to \'Version: selfcustody/krux/releases/tag/v24.03.0\'', async () => {
-    await instance.mainSelectVersionText.waitForExist()
-    await expect(instance.mainSelectVersionText).toBeDisplayed()
-    await expect(instance.mainSelectVersionText).toHaveText('Version: selfcustody/krux/releases/tag/v24.03.0')
-  })
-})
diff --git a/test/e2e/specs/034.already-downloaded-selfcustody-pem-show-details-button.spec.mts b/test/e2e/specs/034.already-downloaded-selfcustody-pem-show-details-button.spec.mts
deleted file mode 100644
index d2ffbefc..00000000
--- a/test/e2e/specs/034.already-downloaded-selfcustody-pem-show-details-button.spec.mts
+++ /dev/null
@@ -1,152 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { join } from 'path'
-import { homedir } from 'os'
-import { osLangSync } from 'os-lang'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectVersion page (already downloaded public key certificate - click show details button)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await instance.selectVersionPage.waitForExist()
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await instance.selectVersionOdudexButton.waitForExist()
-    await instance.selectVersionOdudexText.waitForExist()
-    await instance.selectVersionSelfcustodyButton.click()
-    await instance.selectVersionPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipMsg.waitForExist()
-    await instance.foundReleaseZipMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningAlreadyDownloadedText.waitForExist() 
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSha256txtMsg.waitForExist()
-    await instance.foundReleaseZipSha256txtMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSigMsg.waitForExist()
-    await instance.foundReleaseZipSigMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleasePemMsg.waitForExist()
-    await instance.foundReleasePemMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-  })
-
-  it ('should overlay not be shown', async () => {
-    await expect(instance.warningAlreadyDownloadedOverlay).not.toBeDisplayed()
-  })
-
-  it ('should click \'Show details\' button and overlay must be visible', async () => {
-    await instance.warningDownloadShowDetailsButton.click()
-    await instance.warningAlreadyDownloadedOverlay.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlay).toBeDisplayed()
-  })
-
-  it('should overlay title be \'Resource details\'', async () => {
-    await instance.warningAlreadyDownloadedOverlayTitle.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlayTitle).toBeDisplayed()
-    await expect(instance.warningAlreadyDownloadedOverlayTitle).toHaveText('Resource details')
-  })
-
-  it ('should overlay subtitle be \'main/selfcustody.pem\'', async () => {
-    await instance.warningAlreadyDownloadedOverlayTitle.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlaySubtitle).toBeDisplayed()
-    await expect(instance.warningAlreadyDownloadedOverlaySubtitle).toHaveText('main/selfcustody.pem')
-  })
-
-  it ('should a overlay text have \'Remote: https://raw.githubusercontent.com/selfcustody/krux/main/selfcustody.pem\'', async () => {
-    await instance.warningAlreadyDownloadedOverlayTextRemote.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlayTextRemote).toBeDisplayed()
-    await expect(instance.warningAlreadyDownloadedOverlayTextRemote).toHaveText('Remote:\nhttps://raw.githubusercontent.com/selfcustody/krux/main/selfcustody.pem')
-  })
-
-  it ('should a overlay text have properly local resource', async () => {
-    await instance.warningAlreadyDownloadedOverlayTextLocal.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlayTextLocal).toBeDisplayed()
-    
-    let resources = ''
-    if (process.env.CI && process.env.GITHUB_ACTION) {
-      if (process.platform  === 'linux') {
-        resources = '/home/runner/krux-installer'
-      } else if (process.platform  === 'win32') {
-        resources = 'C:\\Users\\runneradmin\\Documents\\krux-installer'
-      } else if (process.platform  === 'darwin') {
-        resources = '/Users/runner/Documents/krux-installer'
-      }
-    } else {
-      const lang = osLangSync()
-      const home = homedir()
-      if (lang.match(/en-*/g)) {
-        resources = join(home, 'Documents', 'krux-installer')
-      } else if ( lang.match(/pt-*/g)) {
-        resources = join(home, 'Documentos', 'krux-installer')
-      } else {
-        throw new Error(`${lang} not implemented. Please implement it with correct \'Documents\' folder name`)
-      }
-    }
-
-    const resource = join(resources, 'main', 'selfcustody.pem')
-    await expect(instance.warningAlreadyDownloadedOverlayTextLocal).toHaveText(`Local:\n${resource}`)
-  })
-
-  it('should a overlay text have the properly description', async () => {
-    await instance.warningAlreadyDownloadedOverlayTextWhatdo.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlayTextWhatdo).toBeDisplayed()
-    await expect(instance.warningAlreadyDownloadedOverlayTextWhatdo).toHaveText('Description:\nThis file, with the signature, proves the authenticity of zip file, checking if the zip file was signed by its creator.')
-  })
-  
-  it('should \'close\' have \'Close\' text',async () => {
-    await instance.warningAlreadyDownloadedOverlayButtonClose.waitForExist()
-    await expect(instance.warningAlreadyDownloadedOverlayButtonClose).toBeDisplayed()
-    await expect(instance.warningAlreadyDownloadedOverlayButtonClose).toHaveText('Close')
-  })
-
-  it('should \'close\' button make overlay not visible', async () => {
-    await instance.warningAlreadyDownloadedOverlayButtonClose.click()
-    await instance.warningAlreadyDownloadedOverlay.waitForExist({ reverse: true })
-    await expect(instance.warningAlreadyDownloadedOverlay).not.toBeDisplayed()
-  })
-})
\ No newline at end of file
diff --git a/test/e2e/specs/035.already-downloaded-selfcustody-pem-download-again-button.spec.mts b/test/e2e/specs/035.already-downloaded-selfcustody-pem-download-again-button.spec.mts
deleted file mode 100644
index ea7a203b..00000000
--- a/test/e2e/specs/035.already-downloaded-selfcustody-pem-download-again-button.spec.mts
+++ /dev/null
@@ -1,111 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectVersion page (already downloaded public key certificate - click download again button)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await instance.selectVersionPage.waitForExist()
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await instance.selectVersionOdudexButton.waitForExist()
-    await instance.selectVersionOdudexText.waitForExist()
-    await instance.selectVersionSelfcustodyButton.click()
-    await instance.selectVersionPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipMsg.waitForExist()
-    await instance.foundReleaseZipMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningAlreadyDownloadedText.waitForExist() 
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSha256txtMsg.waitForExist()
-    await instance.foundReleaseZipSha256txtMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSigMsg.waitForExist()
-    await instance.foundReleaseZipSigMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleasePemMsg.waitForExist()
-    await instance.foundReleasePemMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-  })
-
-  it ('should click \'Download again\' go out of WarningDownload page', async () => {
-    await instance.warningDownloadAgainButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await expect(instance.warningDownloadPage).not.toBeDisplayed()
-  })
-
-  it ('should be in DownloadOfficialReleasePem page', async () => {
-    await instance.downloadOfficialReleasePemPage.waitForExist()
-    await expect(instance.downloadOfficialReleasePemPage).toBeDisplayed()
-  })
-
-  it('should DownloadOfficialReleasePem page have \'Downloading\' title', async () => {
-    await instance.downloadOfficialReleasePemTitle.waitForExist()
-    await expect(instance.downloadOfficialReleasePemTitle).toBeDisplayed()
-    await expect(instance.downloadOfficialReleasePemTitle).toHaveText('Downloading')
-  })
-
-  it('should DownloadOfficialReleasePem page have \'https://raw.githubusercontent.com/selfcustody/krux/main/selfcustody.pem\' subtitle', async () => {
-    await instance.downloadOfficialReleasePemSubtitle.waitForExist()
-    await expect(instance.downloadOfficialReleasePemSubtitle).toBeDisplayed()
-    await expect(instance.downloadOfficialReleasePemSubtitle).toHaveText('https://raw.githubusercontent.com/selfcustody/krux/main/selfcustody.pem')
-  })
-
-  it('should DownloadOfficialReleasePem page progress until 100%', async () => {
-    await instance.downloadOfficialReleasePemProgress.waitForExist()
-    await expect(instance.downloadOfficialReleasePemProgress).toBeDisplayed()
-    // TODO: Pem is so small that wdio cannot check progress 
-    /*await instance.downloadOfficialReleasePemProgress.waitUntil(async function () {
-      const percentText = await this.getText()
-      const percent = parseFloat(percentText.split('%')[0])
-      return percent === 100.00
-    }, {
-      timeout: 60000,
-      interval: 50
-    })*/
-  })
-})
\ No newline at end of file
diff --git a/test/e2e/specs/036.already-downloaded-selfcustody-pem-click-proceed-button.spec.mts b/test/e2e/specs/036.already-downloaded-selfcustody-pem-click-proceed-button.spec.mts
deleted file mode 100644
index 344be9da..00000000
--- a/test/e2e/specs/036.already-downloaded-selfcustody-pem-click-proceed-button.spec.mts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-describe('KruxInstaller SelectVersion page (already downloaded public key certificate - click proceed again button)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await instance.selectVersionPage.waitForExist()
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await instance.selectVersionOdudexButton.waitForExist()
-    await instance.selectVersionOdudexText.waitForExist()
-    await instance.selectVersionSelfcustodyButton.click()
-    await instance.selectVersionPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipMsg.waitForExist()
-    await instance.foundReleaseZipMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningAlreadyDownloadedText.waitForExist() 
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSha256txtMsg.waitForExist()
-    await instance.foundReleaseZipSha256txtMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSigMsg.waitForExist()
-    await instance.foundReleaseZipSigMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleasePemMsg.waitForExist()
-    await instance.foundReleasePemMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-  })
-  
-  it ('should click \'Proceed\' go out of WarningDownload page', async () => {
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await expect(instance.warningDownloadPage).not.toBeDisplayed()
-  })
-
-})
\ No newline at end of file
diff --git a/test/e2e/specs/037-check-verify-official-release.spec.mts b/test/e2e/specs/037-check-verify-official-release.spec.mts
deleted file mode 100644
index 78a0fc06..00000000
--- a/test/e2e/specs/037-check-verify-official-release.spec.mts
+++ /dev/null
@@ -1,121 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-const LEN_SASSAMAN = [
-    ":.:'' ,,xiW,\"4x, ''           ",
-    "         :  ,dWWWXXXXi,4WX,            ",
-    "         ' dWWWXXX7\"     `X,           ",
-    "         lWWWXX7   __   _ X            ",
-    "         :WWWXX7 ,xXX7' \"^^X           ",
-    "         lWWWX7, _.+,, _.+.,           ",
-    "         :WWW7,. `^\"-\" ,^-'            ",
-    "         WW\",X:        X,              ",
-    "         \"7^^Xl.    _(_x7'             ",
-    "         l ( :X:       __ _            ",
-    "         `. \" XX  ,xxWWWWX7            ",
-    "           )X- \"\" 4X\" .___.            ",
-    "         ,W X     :Xi  _,,_            ",
-    "         WW X      4XiyXWWXd           ",
-    "         \"\" ,,      4XWWWWXX           ",
-    "         , R7X,       \"^447^           ",
-    "         R, \"4RXk,      _, ,           ",
-    "         TWk  \"4RXXi,   X',x           ",
-    "         lTWk,  \"4RRR7' 4 XH           ",
-    "         :lWWWk,  ^\"     `4            ",
-    "         ::TTXWWi,_  Xll :..           ",
-    "                                       ",
-    "   Len Sassaman is using openssl to    ",
-    "   verify sha256sum and signature..." 
-].join('\n')
-
-describe('KruxInstaller CheckVerifyOfficialRelease page (show Lensassaman \'using\' openssl)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await instance.selectVersionPage.waitForExist()
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await instance.selectVersionOdudexButton.waitForExist()
-    await instance.selectVersionOdudexText.waitForExist()
-    await instance.selectVersionSelfcustodyButton.click()
-    await instance.selectVersionPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipMsg.waitForExist()
-    await instance.foundReleaseZipMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningAlreadyDownloadedText.waitForExist() 
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSha256txtMsg.waitForExist()
-    await instance.foundReleaseZipSha256txtMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSigMsg.waitForExist()
-    await instance.foundReleaseZipSigMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleasePemMsg.waitForExist()
-    await instance.foundReleasePemMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-  })
-
-  it('should go to CheckVerifyOfficialRelease page', async () => { 
-    await instance.checkVerifyOfficialReleasePage.waitForExist({ timeout: 3000 })
-    await expect(instance.checkVerifyOfficialReleasePage).toBeDisplayed()
-  })
-
-  it('should show correctly Len Sassaman \'verifying\' with openssl', async () => {
-    await instance.checkVerifyOfficialReleaseLenSassamanIsUsingOpenssl.waitForExist()
-    await expect(instance.checkVerifyOfficialReleaseLenSassamanIsUsingOpenssl).toHaveText(LEN_SASSAMAN)
-  })
-
-  it('should go out of CheckVerifyOfficialRelease page', async () => {
-    await instance.checkVerifyOfficialReleaseLenSassamanIsUsingOpenssl.waitForExist({ reverse: true })
-    await instance.checkVerifyOfficialReleasePage.waitForExist({ reverse: true })
-  })
-  
-})
\ No newline at end of file
diff --git a/test/e2e/specs/038-verified-official-release.spec.mts b/test/e2e/specs/038-verified-official-release.spec.mts
deleted file mode 100644
index 502a0b48..00000000
--- a/test/e2e/specs/038-verified-official-release.spec.mts
+++ /dev/null
@@ -1,189 +0,0 @@
-import { expect } from '@wdio/globals'
-import { describe, it } from 'mocha'
-import { join } from 'path'
-import { homedir } from 'os'
-import { osLangSync } from 'os-lang'
-import { createRequire } from 'module'
-
-const App = createRequire(import.meta.url)('../pageobjects/app.page')
-
-const SHA256 = "f2 54 69 2f 76 6d c6 b0 09 c8 ca 7f 43 b6 74 d0 88 06 26 85 bb 20 3b 85 0f 8b 70 2f 64 1b 59 35"
-describe('KruxInstaller VerifiedOfficialRelease page (show and click back button)', () => {
-
-  let instance: any;
-
-  before(async function () {
-    instance = new App()
-    await instance.app.waitForExist()
-    await instance.main.waitForExist()
-    await instance.logo.waitForExist()
-    await instance.logo.waitForExist({ reverse: true })
-    await instance.loadingDataMsg.waitForExist()
-    await instance.verifyingOpensslMsg.waitForExist()
-    if (process.platform === 'linux') {
-      await instance.opensslForLinuxFound.waitForExist()
-    } else if (process.platform === 'darwin') {
-      await instance.opensslForDarwinFound.waitForExist()
-    } else if (process.platform === 'win32') {
-      await instance.opensslForWin32Found.waitForExist()
-    }
-    await instance.loadingDataMsg.waitForExist({ reverse: true })
-    await instance.verifyingOpensslMsg.waitForExist({ reverse: true })
-    await instance.opensslForLinuxFound.waitForExist({ reverse: true })
-    await instance.mainPage.waitForExist()
-    await instance.mainSelectDeviceButton.waitForExist()
-    await instance.mainSelectVersionButton.waitForExist()
-    await instance.mainSelectVersionButton.click()
-    await instance.mainPage.waitForExist({ reverse: true })
-    await instance.githubOctocatCheckerLogo.waitForExist({ timeout: 3000 })
-    await instance.selectVersionPage.waitForExist()
-    await instance.selectVersionSelfcustodyButton.waitForExist()
-    await instance.selectVersionSelfcustodyText.waitForExist()
-    await instance.selectVersionOdudexButton.waitForExist()
-    await instance.selectVersionOdudexText.waitForExist()
-    await instance.selectVersionSelfcustodyButton.click()
-    await instance.selectVersionPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipMsg.waitForExist()
-    await instance.foundReleaseZipMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningAlreadyDownloadedText.waitForExist() 
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadAgainButton.waitForExist()
-    await instance.warningDownloadAgainButtonText.waitForExist()
-    await instance.warningDownloadShowDetailsButton.waitForExist()
-    await instance.warningDownloadShowDetailsButtonText.waitForExist()
-    await instance.warningDownloadBackButton.waitForExist()
-    await instance.warningDownloadBackButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSha256txtMsg.waitForExist()
-    await instance.foundReleaseZipSha256txtMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleaseZipSigMsg.waitForExist()
-    await instance.foundReleaseZipSigMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkingReleasePemMsg.waitForExist()
-    await instance.foundReleasePemMsg.waitForExist()
-    await instance.warningDownloadPage.waitForExist()
-    await instance.warningDownloadProceedButton.waitForExist()
-    await instance.warningDownloadProceedButtonText.waitForExist()
-    await instance.warningDownloadProceedButton.click()
-    await instance.warningDownloadPage.waitForExist({ reverse: true })
-    await instance.checkVerifyOfficialReleasePage.waitForExist({ timeout: 3000 })
-    await instance.checkVerifyOfficialReleaseLenSassamanIsUsingOpenssl.waitForExist()
-    await instance.checkVerifyOfficialReleaseLenSassamanIsUsingOpenssl.waitForExist({ reverse: true })
-    await instance.checkVerifyOfficialReleasePage.waitForExist({ reverse: true })
-  })
-
-  it('should show VerifiedOfficialRelease page', async () => {
-    await instance.verifiedOfficialReleasePage.waitForExist()
-    await expect(instance.verifiedOfficialReleasePage).toBeDisplayed()
-  })
-
-  it('should show sha256sum intergrity title', async () => {
-    await instance.verifiedOfficialReleasePageSha2256IntegrityTitle.waitForExist()
-    await expect(instance.verifiedOfficialReleasePageSha2256IntegrityTitle).toBeDisplayed()     
-    await expect(instance.verifiedOfficialReleasePageSha2256IntegrityTitle).toHaveText('Sha256sum integrity')
-  })
-
-  it('should show sha256sum intergrity sha256.txt', async () => {
-    await instance.verifiedOfficialReleasePageSha2256IntegritySha256txt.waitForExist()
-    await expect(instance.verifiedOfficialReleasePageSha2256IntegritySha256txt).toBeDisplayed()
-    
-    await expect(instance.verifiedOfficialReleasePageSha2256IntegritySha256txt).toHaveText(`Expected result from file v24.03.0/krux-v24.03.0.zip.sha256.txt\n${SHA256}`)
-  })
-
-  it('should show sha256sum intergrity sha256 summed result', async () => {
-    await instance.verifiedOfficialReleasePageSha2256IntegritySha256.waitForExist()
-    await expect(instance.verifiedOfficialReleasePageSha2256IntegritySha256).toBeDisplayed()
-    await expect(instance.verifiedOfficialReleasePageSha2256IntegritySha256).toHaveText(`Summed result of file v24.03.0/krux-v24.03.0.zip\n${SHA256}`)
-  })
-  
-  it('should show openssl authenticity title', async () => {
-    await instance.verifiedOfficialReleasePageSignatureTitle.waitForExist()
-    await expect(instance.verifiedOfficialReleasePageSignatureTitle).toBeDisplayed()     
-    await expect(instance.verifiedOfficialReleasePageSignatureTitle).toHaveText('Signature authenticity')
-  })
-
-  it('should show openssl authenticity command', async () => {
-    await instance.verifiedOfficialReleasePageSignatureCommand.waitForExist()
-    await expect(instance.verifiedOfficialReleasePageSignatureCommand).toBeDisplayed()
-    
-    let resources = ''
-    let openssl = ''
-    if (process.env.CI && process.env.GITHUB_ACTION) {
-      if (process.platform  === 'linux') {
-        resources = '/home/runner/krux-installer'
-        openssl = 'openssl'
-      } else if (process.platform  === 'win32') {
-        resources = 'C:\\Users\\runneradmin\\Documents\\krux-installer'
-        openssl = 'openssl.exe'
-      } else if (process.platform  === 'darwin') {
-        resources = '/Users/runner/Documents/krux-installer'
-        openssl = 'openssl'
-      }
-    } else {
-      const lang = osLangSync()
-      const home = homedir()
-      if (process.platform  === 'linux' || process.platform === 'darwin') {
-        openssl = 'openssl'
-      } else if (process.platform  === 'win32') {
-        openssl = 'openssl.exe'
-      }
-      if ( lang.match(/en-*/g)) {
-        resources = join(home, 'Documents', 'krux-installer')
-      } else if ( lang.match(/pt-*/g)) {
-        resources = join(home, 'Documentos', 'krux-installer')
-      } else {
-        throw new Error(`${lang} not implemented. Please implement it with correct \'Documents\' folder name`)
-      }
-    }
-
-    const resourceZip = join(resources, 'v24.03.0', 'krux-v24.03.0.zip')
-    const resourcePem = join(resources, 'main', 'selfcustody.pem')
-    const resourceSig = join(resources, 'v24.03.0', 'krux-v24.03.0.zip.sig')
-    const command = [
-      '$>',
-      `${openssl} sha256 <${resourceZip}`,
-      '-binary',
-      '|',
-      openssl,
-      'pkeyutl',
-      '-verify',
-      '-pubin',
-      '-inkey',
-      resourcePem,
-      '-sigfile',
-      resourceSig
-    ].join(' ')
-
-    await expect(instance.verifiedOfficialReleasePageSignatureCommand).toHaveText(command)
-  })
-
-  it('should show openssl authenticity command result', async () => {
-    await instance.verifiedOfficialReleasePageSignatureResult.waitForExist()
-    await expect(instance.verifiedOfficialReleasePageSignatureResult).toBeDisplayed()     
-    await expect(instance.verifiedOfficialReleasePageSignatureResult).toHaveText('Signature Verified Successfully')
-  })
-
-  it('should show back button', async () => {
-    await instance.verifiedOfficialReleasePageBackButton.waitForExist()
-    await expect(instance.verifiedOfficialReleasePageBackButton).toBeDisplayed()     
-    await expect(instance.verifiedOfficialReleasePageBackButton).toHaveText('Back')
-  })
-
-  it('should click back button, exit from VerifiedOfficialRelease page', async () => {
-    await instance.verifiedOfficialReleasePageBackButton.click()
-    await instance.verifiedOfficialReleasePage.waitForExist({ reverse: true })
-  })
-
-})
diff --git a/test/e2e/utils/index.ts b/test/e2e/utils/index.ts
deleted file mode 100644
index b5b867dc..00000000
--- a/test/e2e/utils/index.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-const { join } = require('path')
-const { readFile } = require('fs/promises')
-const { browser } = require('@wdio/globals')
-
-exports.delay = function (ms: number): Promise {
-  return new Promise((resolve) => {
-    return setTimeout(resolve, ms)
-  })
-}
-
-// Correct way to convert size in bytes to KB, MB, GB in JavaScript
-// https://gist.github.com/lanqy/5193417?permalink_comment_id=4225701#gistcomment-4225701
-exports.formatBytes = function (bytes: number): string {
-  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
-  if (bytes === 0) return 'n/a'
-  const n = Math.log(bytes as number) / Math.log(1024)
-  const i = Math.min(Math.floor(n), sizes.length - 1)
-  if (i === 0) return `${bytes} ${sizes[i]}`
-  return `${(bytes / (1024 ** i)).toFixed(2)} ${sizes[i]}` as string
-}
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/mock.png b/tests/mock.png
new file mode 100644
index 00000000..93239652
Binary files /dev/null and b/tests/mock.png differ
diff --git a/tests/pubkey.png b/tests/pubkey.png
new file mode 100644
index 00000000..ea1f5b62
Binary files /dev/null and b/tests/pubkey.png differ
diff --git a/tests/shared_mocks.py b/tests/shared_mocks.py
new file mode 100644
index 00000000..cc6c8d7c
--- /dev/null
+++ b/tests/shared_mocks.py
@@ -0,0 +1,169 @@
+"""
+shared_mocks.py
+
+authors[PropertyInstanceMock]:
+Don Kirkby, Intrastellar Explorer
+
+authors[MockZipFile]:
+nneonneo
+
+edited by: qlrd
+
+Changes[PropertyInstanceMock]:
+
+- added `obj_type=None` to avoid pylint warning
+W0222: Signature differs from overridden
+'__get__' method (signature-differs)
+
+- added `_get_child_mock` to mimic class like
+https://github.com/python/cpython/blob/
+main/Lib/unittest/mock.py
+
+Get from https://stackoverflow.com/
+questions/37553552/assert-that-a-propertymock-
+was-called-on-a-specific-instance
+"""
+
+import typing
+import sys
+from unittest.mock import Mock, MagicMock, PropertyMock
+
+
+class PropertyInstanceMock(PropertyMock):
+    """Like PropertyMock, but records the instance that was called."""
+
+    def _get_child_mock(self, /, **kwargs):
+        """return a MagicMock"""
+        return MagicMock(**kwargs)
+
+    def __get__(self, obj, obj_type=None):
+        """Return a Getter of @property"""
+        return self(obj)
+
+    def __set__(self, obj, val):
+        """Return a setter of @property.setter"""
+        self(obj, val)
+
+
+class MockZipFile:
+    """
+    Instead of having mockZip.__enter__ return an empty list,
+    have it return an object like the following
+    """
+
+    def __init__(self):
+        self.files = [
+            Mock(filename="README.md"),
+            Mock(filename="pyproject.toml"),
+            Mock(filename="LICENSE"),
+            Mock(filename=".pylintrc"),
+        ]
+
+    @property
+    def files(self):
+        return self._files
+
+    @files.setter
+    def files(self, value: typing.List[Mock]):
+        self._files = value
+
+    def __iter__(self):
+        return iter(self.files)
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        return True
+
+    def infolist(self):
+        return self.files
+
+    def namelist(self):
+        return [data.filename for data in self.files]
+
+    def extract(self, name: str, path: str):
+        pass
+
+
+class MockKruxZipFile(MockZipFile):
+
+    def __init__(self):
+        super().__init__()
+        self.files = [
+            Mock(filename="test/"),
+            Mock(filename="test/maixpy_m5stickv"),
+            Mock(filename="test/maixpy_m5stickv/firmware.bin"),
+            Mock(filename="test/maixpy_m5stickv/firmware.bin.sig"),
+            Mock(filename="test/maixpy_m5stickv/kboot.kfpkg"),
+            Mock(filename="test/maixpy_amigo_tft"),
+            Mock(filename="test/maixpy_amigo_tft/firmware.bin"),
+            Mock(filename="test/maixpy_amigo_tft/firmware.bin.sig"),
+            Mock(filename="test/maixpy_amigo_tft/kboot.kfpkg"),
+            Mock(filename="test/maixpy_amigo_ips"),
+            Mock(filename="test/maixpy_amigo_ips/firmware.bin"),
+            Mock(filename="test/maixpy_amigo_ips/firmware.bin.sig"),
+            Mock(filename="test/maixpy_amigo_ips/kboot.kfpkg"),
+            Mock(filename="test/maixpy_dock"),
+            Mock(filename="test/maixpy_dock/firmware.bin"),
+            Mock(filename="test/maixpy_dock/firmware.bin.sig"),
+            Mock(filename="test/maixpy_dock/kboot.kfpkg"),
+            Mock(filename="test/maixpy_bit"),
+            Mock(filename="test/maixpy_bit/firmware.bin"),
+            Mock(filename="test/maixpy_bit/firmware.bin.sig"),
+            Mock(filename="test/maixpy_bit/kboot.kfpkg"),
+            Mock(filename="test/maixpy_yahboom"),
+            Mock(filename="test/maixpy_yahboom/firmware.bin"),
+            Mock(filename="test/maixpy_yahboom/firmware.bin.sig"),
+            Mock(filename="test/maixpy_yahboom/kboot.kfpkg"),
+        ]
+
+
+class MonkeyPort:
+
+    # pylint: disable=too-few-public-methods
+    def __init__(self, vid: str, device: str):
+        self.vid = vid
+        self.device = device
+
+
+class MockListPortsGrep(MagicMock):
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.devices = []
+
+        if sys.platform in ("linux", "darwin"):
+            self.devices = [
+                MagicMock(vid="0403", device="/mock/path0"),
+                MagicMock(vid="0403", device="/mock/path1"),
+                MagicMock(vid="7523", device="/mock/path0"),
+            ]
+        elif sys.platform == "win32":
+            self.devices = [
+                MagicMock(vid="0403", device="MOCK0"),
+                MagicMock(vid="0403", device="MOCK1"),
+                MagicMock(vid="7523", device="MOCK0"),
+            ]
+
+
+class MockSerial(MagicMock):
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+    def open(self):
+        pass
+
+    def close(self):
+        pass
+
+
+MOCKED_SIGNATURE = "".join(
+    [
+        "MEQCIC2VjiRUu/UyjDlfQJCrA8Yy",
+        "PE8gxqZXslsqck3N6t/2AiBj0hvV",
+        "6lpczTW4CoaBGlmQB/0yKice5BUF",
+        "6xwHQRWvow==",
+    ]
+)
diff --git a/tests/signature.png b/tests/signature.png
new file mode 100644
index 00000000..d3b8816c
Binary files /dev/null and b/tests/signature.png differ
diff --git a/tests/test_000_constants.py b/tests/test_000_constants.py
new file mode 100644
index 00000000..de93c93a
--- /dev/null
+++ b/tests/test_000_constants.py
@@ -0,0 +1,111 @@
+import os
+from unittest import TestCase
+from unittest.mock import mock_open, patch
+from src.utils.constants import _open_pyproject, get_name, get_version, get_description
+
+PYPROJECT_STR = """
+[tool.poetry]
+name = "test"
+version = "0.0.1"
+description = "Hello World!"
+"""
+
+
+class TestConstants(TestCase):
+
+    @patch("sys.version_info")
+    @patch("builtins.open", new_callable=mock_open, read_data=PYPROJECT_STR)
+    def test_open_pyproject_with_py_minor_version_10(
+        self, open_mock, mock_version_info
+    ):
+        mock_version_info.minor = 9
+
+        rootdirname = os.path.abspath(os.path.dirname(__file__))
+        pyproject_filename = os.path.abspath(
+            os.path.join(rootdirname, "..", "pyproject.toml")
+        )
+
+        data = _open_pyproject()
+        open_mock.assert_called_once_with(pyproject_filename, "r", encoding="utf8")
+        self.assertEqual(
+            data,
+            {
+                "tool": {
+                    "poetry": {
+                        "name": "test",
+                        "version": "0.0.1",
+                        "description": "Hello World!",
+                    }
+                }
+            },
+        )
+
+    @patch("builtins.open", new_callable=mock_open, read_data=PYPROJECT_STR)
+    def test_open_pyproject(self, open_mock):
+        rootdirname = os.path.abspath(os.path.dirname(__file__))
+        pyproject_filename = os.path.abspath(
+            os.path.join(rootdirname, "..", "pyproject.toml")
+        )
+
+        data = _open_pyproject()
+        open_mock.assert_called_once_with(pyproject_filename, "r", encoding="utf8")
+        self.assertEqual(
+            data,
+            {
+                "tool": {
+                    "poetry": {
+                        "name": "test",
+                        "version": "0.0.1",
+                        "description": "Hello World!",
+                    }
+                }
+            },
+        )
+
+    @patch("builtins.open", new_callable=mock_open, read_data=PYPROJECT_STR)
+    def test_fail_open_pyproject_file_not_found(self, open_mock):
+        open_mock.side_effect = FileNotFoundError
+
+        rootdirname = os.path.abspath(os.path.dirname(__file__))
+        pyproject_filename = os.path.abspath(
+            os.path.join(rootdirname, "..", "pyproject.toml")
+        )
+
+        with self.assertRaises(FileNotFoundError) as exc_info:
+            _open_pyproject()
+
+        self.assertEqual(str(exc_info.exception), f"{pyproject_filename} isnt found")
+
+    @patch("builtins.open", new_callable=mock_open, read_data=PYPROJECT_STR)
+    def test_fail_open_pyproject_value_error(self, open_mock):
+        open_mock.side_effect = ValueError
+
+        rootdirname = os.path.abspath(os.path.dirname(__file__))
+        pyproject_filename = os.path.abspath(
+            os.path.join(rootdirname, "..", "pyproject.toml")
+        )
+
+        with self.assertRaises(ValueError) as exc_info:
+            _open_pyproject()
+
+        self.assertEqual(
+            str(exc_info.exception), f"{pyproject_filename} is not valid toml file"
+        )
+
+    @patch("builtins.open", new_callable=mock_open, read_data=PYPROJECT_STR)
+    def test_get_name(self, open_mock):
+        name = get_name()
+        open_mock.assert_called_once()
+        self.assertEqual(name, "test")
+
+    @patch("builtins.open", new_callable=mock_open, read_data=PYPROJECT_STR)
+    def test_get_version(self, open_mock):
+        version = get_version()
+        open_mock.assert_called_once()
+        self.assertEqual(version, "0.0.1")
+
+    @patch("builtins.open", new_callable=mock_open, read_data=PYPROJECT_STR)
+    def test_007_ensure_get_correct_description(self, open_mock):
+        description = get_description()
+        open_mock.assert_called_once()
+        self.assertEqual(description, "Hello World!")
diff --git a/tests/test_001_info.py b/tests/test_001_info.py
new file mode 100644
index 00000000..b3fda89e
--- /dev/null
+++ b/tests/test_001_info.py
@@ -0,0 +1,54 @@
+from unittest import TestCase
+from unittest.mock import MagicMock, patch
+from src.utils.info import mro
+
+
+class TestInfo(TestCase):
+
+    @patch("src.utils.info.currentframe")
+    def test_fail_co_varnames(self, mock_currentframe):
+        mock_f_code = MagicMock()
+        mock_f_code.co_varnames = []
+
+        mock_f_back = MagicMock()
+        mock_f_back.f_code = mock_f_code
+
+        mock_currentframe.return_value = mock_f_back
+
+        m = mro()
+        mock_currentframe.assert_called_once()
+        self.assertEqual(m, None)
+
+    @patch("src.utils.info.currentframe")
+    def test_fail_f_locals(self, mock_currentframe):
+        mock_f_locals = MagicMock()
+        mock_f_locals.side_effect = KeyError
+
+        mock_f_code = MagicMock()
+        mock_f_code.co_varnames = ["mock"]
+
+        mock_f_back = MagicMock()
+        mock_f_back.f_code = mock_f_code
+
+        mock_currentframe.return_value = mock_f_back
+
+        m = mro()
+        mock_currentframe.assert_called_once()
+        self.assertEqual(m, None)
+
+    @patch("src.utils.info.currentframe")
+    def test_empty_cls(self, mock_currentframe):
+        mock_f_locals = MagicMock()
+        mock_f_locals.return_value = {"mock": "test"}
+
+        mock_f_code = MagicMock()
+        mock_f_code.co_varnames = ["mock"]
+
+        mock_f_back = MagicMock()
+        mock_f_back.f_code = mock_f_code
+
+        mock_currentframe.return_value = mock_f_back
+
+        m = mro()
+        mock_currentframe.assert_called_once()
+        self.assertEqual(m, None)
diff --git a/tests/test_002_trigger.py b/tests/test_002_trigger.py
new file mode 100644
index 00000000..116b775e
--- /dev/null
+++ b/tests/test_002_trigger.py
@@ -0,0 +1,41 @@
+import os
+from unittest import TestCase
+from unittest.mock import patch
+from src.utils.trigger import Trigger
+
+
+class TestTrigger(TestCase):
+
+    @patch("src.utils.trigger.mro", return_value="Mock")
+    def test_info(self, mock_mro):
+        t = Trigger()
+        t.info("Hello World")
+        mock_mro.assert_called_once()
+
+    @patch.dict(os.environ, {"LOGLEVEL": "warning"}, clear=True)
+    @patch("src.utils.trigger.mro", return_value="Mock")
+    def test_warn(self, mock_mro):
+        trigger = Trigger()
+        trigger.warning("Hello World")
+        mock_mro.assert_called_once()
+
+    @patch.dict(os.environ, {"LOGLEVEL": "warning"}, clear=True)
+    @patch("src.utils.trigger.mro", return_value="Mock")
+    def test_error(self, mock_mro):
+        trigger = Trigger()
+        trigger.error("Hello World")
+        mock_mro.assert_called_once()
+
+    @patch.dict(os.environ, {"LOGLEVEL": "debug"}, clear=True)
+    @patch("src.utils.trigger.mro", return_value="Mock")
+    def test_debug(self, mock_mro):
+        trigger = Trigger()
+        trigger.debug("Hello World")
+        mock_mro.assert_called_once()
+
+    @patch.dict(os.environ, {"LOGLEVEL": "critical"}, clear=True)
+    @patch("src.utils.trigger.mro", return_value="Mock")
+    def test_critical(self, mock_mro):
+        trigger = Trigger()
+        trigger.critical("Hello World")
+        mock_mro.assert_called_once()
diff --git a/tests/test_003_selector.py b/tests/test_003_selector.py
new file mode 100644
index 00000000..0fcc4f8b
--- /dev/null
+++ b/tests/test_003_selector.py
@@ -0,0 +1,178 @@
+from unittest import TestCase
+from unittest.mock import MagicMock, patch
+import requests
+from src.utils.selector import Selector
+
+
+MOCKED_EMPTY_API = []
+MOCKED_WRONG_API = [
+    {"author": "test"},
+    {"author": "test"},
+    {"author": "test"},
+]
+MOCKED_FOUND_API = [
+    {"author": "test", "tag_name": "v0.0.1"},
+    {"author": "test", "tag_name": "v0.1.0"},
+    {"author": "test", "tag_name": "v1.0.0"},
+]
+
+
+class TestSelector(TestCase):
+
+    @patch("src.utils.selector.requests")
+    def test_init(self, mock_requests):
+        mock_response = MagicMock()
+        mock_response.status_code = 200
+        mock_response.json.return_value = MOCKED_FOUND_API
+        mock_requests.get.return_value = mock_response
+
+        selector = Selector()
+
+        mock_requests.get.assert_called_once_with(
+            url="https://api.github.com/repos/selfcustody/krux/releases",
+            headers={
+                "Accept": "application/vnd.github+json",
+                "X-GitHub-Api-Version": "2022-11-28",
+            },
+            timeout=10,
+        )
+
+        self.assertEqual(selector.releases[0], "v0.0.1")
+        self.assertEqual(selector.releases[1], "v0.1.0")
+        self.assertEqual(selector.releases[2], "v1.0.0")
+
+    @patch("src.utils.selector.requests")
+    def test_fail_init_empty_data(self, mock_requests):
+        mock_response = MagicMock()
+        mock_response.status_code = 200
+        mock_response.json.return_value = MOCKED_EMPTY_API
+        mock_requests.get.return_value = mock_response
+
+        with self.assertRaises(ValueError) as exc_info:
+            Selector()
+
+        self.assertEqual(
+            str(exc_info.exception),
+            "https://api.github.com/repos/selfcustody/krux/releases returned empty data",
+        )
+
+    @patch("src.utils.selector.requests")
+    def test_fail_init_wrong_data(self, mock_requests):
+        mock_response = MagicMock()
+        mock_response.status_code = 200
+        mock_response.json.return_value = MOCKED_WRONG_API
+        mock_requests.get.return_value = mock_response
+
+        with self.assertRaises(KeyError) as exc_info:
+            Selector()
+
+        self.assertEqual(
+            str(exc_info.exception), "\"Invalid key: 'tag_name' do not exist on api\""
+        )
+
+    @patch("src.utils.selector.requests")
+    def test_fail_init_http_error_404(self, mock_requests):
+        mock_response = MagicMock(status_code=404)
+        mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError(
+            "Mocked 404"
+        )
+        mock_requests.exceptions = requests.exceptions
+        mock_requests.get.return_value = mock_response
+
+        with self.assertRaises(RuntimeError) as exc_info:
+            Selector()
+
+        self.assertEqual(str(exc_info.exception), "Mocked 404")
+
+    @patch("src.utils.selector.requests")
+    def test_fail_init_http_error_500(self, mock_requests):
+        mock_response = MagicMock(status_code=500)
+        mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError(
+            "Mocked 500"
+        )
+        mock_requests.exceptions = requests.exceptions
+        mock_requests.get.return_value = mock_response
+
+        with self.assertRaises(RuntimeError) as exc_info:
+            Selector()
+
+        self.assertEqual(str(exc_info.exception), "Mocked 500")
+
+    @patch("src.utils.selector.requests")
+    def test_fail_init_timeout(self, mock_requests):
+        mock_response = MagicMock(status_code=404)
+        mock_response.raise_for_status.side_effect = requests.exceptions.Timeout(
+            "Mocked timeout"
+        )
+        mock_requests.exceptions = requests.exceptions
+        mock_requests.get.return_value = mock_response
+
+        with self.assertRaises(RuntimeError) as exc_info:
+            Selector()
+
+        self.assertEqual(str(exc_info.exception), "Mocked timeout")
+
+    @patch("src.utils.selector.requests")
+    def test_fail_init_http_connection_error(self, mock_requests):
+        mock_response = MagicMock(status_code=404)
+        mock_response.raise_for_status.side_effect = (
+            requests.exceptions.ConnectionError("Mocked connection")
+        )
+        mock_requests.exceptions = requests.exceptions
+        mock_requests.get.return_value = mock_response
+
+        with self.assertRaises(RuntimeError) as exc_info:
+            Selector()
+
+        self.assertEqual(str(exc_info.exception), "Mocked connection")
+
+    @patch("src.utils.selector.requests")
+    def test_set_get_device(self, mock_requests):
+        mock_response = MagicMock()
+        mock_response.status_code = 200
+        mock_response.json.return_value = MOCKED_FOUND_API
+        mock_requests.get.return_value = mock_response
+
+        selector = Selector()
+
+        for device in ("m5stickv", "amigo", "dock", "bit", "yahboom", "cube"):
+            selector.device = device
+            self.assertEqual(selector.device, device)
+
+    @patch("src.utils.selector.requests")
+    def test_fail_set_device(self, mock_requests):
+        mock_response = MagicMock()
+        mock_response.status_code = 200
+        mock_response.json.return_value = MOCKED_FOUND_API
+        mock_requests.get.return_value = mock_response
+
+        with self.assertRaises(ValueError) as exc_info:
+            selector = Selector()
+            selector.device = "mock"
+
+        self.assertEqual(str(exc_info.exception), "Device 'mock' is not valid")
+
+    @patch("src.utils.selector.requests")
+    def test_set_get_firmware(self, mock_requests):
+        mock_response = MagicMock()
+        mock_response.status_code = 200
+        mock_response.json.return_value = MOCKED_FOUND_API
+        mock_requests.get.return_value = mock_response
+
+        selector = Selector()
+        for version in ("v0.0.1", "v0.1.0", "v1.0.0"):
+            selector.firmware = version
+            self.assertTrue(selector.firmware in ("v0.0.1", "v0.1.0", "v1.0.0"))
+
+    @patch("src.utils.selector.requests")
+    def test_fail_set_firmware(self, mock_requests):
+        mock_response = MagicMock()
+        mock_response.status_code = 200
+        mock_response.json.return_value = MOCKED_FOUND_API
+        mock_requests.get.return_value = mock_response
+
+        with self.assertRaises(ValueError) as exc_info:
+            selector = Selector()
+            selector.firmware = "v0.0.111"
+
+        self.assertEqual(str(exc_info.exception), "Firmware 'v0.0.111' is not valid")
diff --git a/tests/test_004_base_downloader.py b/tests/test_004_base_downloader.py
new file mode 100644
index 00000000..7019e463
--- /dev/null
+++ b/tests/test_004_base_downloader.py
@@ -0,0 +1,81 @@
+from unittest import TestCase
+from unittest.mock import patch
+from src.utils.downloader.base_downloader import BaseDownloader
+from .shared_mocks import PropertyInstanceMock
+
+
+class TestBaseDownloader(TestCase):
+
+    @patch(
+        "src.utils.downloader.base_downloader.BaseDownloader.buffer",
+        new_callable=PropertyInstanceMock,
+    )
+    @patch(
+        "src.utils.downloader.base_downloader.BaseDownloader.url",
+        new_callable=PropertyInstanceMock,
+    )
+    def test_init_selfcustody(self, mock_url, mock_buffer):
+        url = "https://github.com/selfcustody/krux"
+        downloader = BaseDownloader(url=url)
+
+        self.assertTrue(downloader.buffer is not None)
+        mock_buffer.assert_called_once_with(downloader)
+        mock_url.assert_called_once_with(downloader, url)
+
+    @patch(
+        "src.utils.downloader.base_downloader.BaseDownloader.buffer",
+        new_callable=PropertyInstanceMock,
+    )
+    @patch(
+        "src.utils.downloader.base_downloader.BaseDownloader.url",
+        new_callable=PropertyInstanceMock,
+    )
+    def test_init_selfcustody_raw(self, mock_url, mock_buffer):
+        url = "https://raw.githubusercontent.com/selfcustody/krux"
+        downloader = BaseDownloader(url=url)
+
+        self.assertTrue(downloader.buffer is not None)
+        mock_buffer.assert_called_once_with(downloader)
+        mock_url.assert_called_once_with(downloader, url)
+
+    @patch(
+        "src.utils.downloader.base_downloader.BaseDownloader.buffer",
+        new_callable=PropertyInstanceMock,
+    )
+    @patch(
+        "src.utils.downloader.base_downloader.BaseDownloader.url",
+        new_callable=PropertyInstanceMock,
+    )
+    def test_init_odudex(self, mock_url, mock_buffer):
+        url = "https://github.com/odudex/krux_binaries"
+        downloader = BaseDownloader(url=url)
+
+        self.assertTrue(downloader.buffer is not None)
+        mock_buffer.assert_called_once_with(downloader)
+        mock_url.assert_called_once_with(downloader, url)
+
+    @patch(
+        "src.utils.downloader.base_downloader.BaseDownloader.buffer",
+        new_callable=PropertyInstanceMock,
+    )
+    @patch(
+        "src.utils.downloader.base_downloader.BaseDownloader.url",
+        new_callable=PropertyInstanceMock,
+    )
+    def test_init_odudex_raw(self, mock_url, mock_buffer):
+        url = "https://raw.githubusercontent.com/odudex/krux_binaries"
+        downloader = BaseDownloader(url=url)
+
+        self.assertTrue(downloader.buffer is not None)
+        mock_buffer.assert_called_once_with(downloader)
+        mock_url.assert_called_once_with(downloader, url)
+
+    def test_fail_init(self):
+        url = "https://gitlab.com/selfcustody/krux"
+
+        with self.assertRaises(ValueError) as exc_info:
+            BaseDownloader(url=url)
+
+        self.assertEqual(
+            str(exc_info.exception), "Invalid url: https://gitlab.com/selfcustody/krux"
+        )
diff --git a/tests/test_005_trigger_downloader.py b/tests/test_005_trigger_downloader.py
new file mode 100644
index 00000000..7a108d9a
--- /dev/null
+++ b/tests/test_005_trigger_downloader.py
@@ -0,0 +1,48 @@
+from unittest import TestCase
+from unittest.mock import patch
+from src.utils.downloader.trigger_downloader import TriggerDownloader
+from .shared_mocks import PropertyInstanceMock
+
+URL = "https://github.com/selfcustody/krux"
+
+
+class TestTriggerDownloader(TestCase):
+
+    @patch(
+        "src.utils.downloader.trigger_downloader.TriggerDownloader.content_len",
+        new_callable=PropertyInstanceMock,
+    )
+    def test_init_content_len(self, mock_content_len):
+        downloader = TriggerDownloader(url=URL)
+        downloader.content_len = 0
+        mock_content_len.assert_called_once_with(downloader, 0)
+
+    @patch(
+        "src.utils.downloader.trigger_downloader.TriggerDownloader.filename",
+        new_callable=PropertyInstanceMock,
+    )
+    def test_init_filename(self, mock_filename):
+        downloader = TriggerDownloader(url=URL)
+        downloader.filename = "mockfile"
+        mock_filename.assert_called_once_with(downloader, "mockfile")
+
+    @patch(
+        "src.utils.downloader.trigger_downloader.TriggerDownloader.downloaded_len",
+        new_callable=PropertyInstanceMock,
+    )
+    def test_init_downloaded_len(self, mock_downloaded_len):
+        downloader = TriggerDownloader(url=URL)
+        downloader.downloaded_len = 0
+        mock_downloaded_len.assert_called_once_with(downloader, 0)
+
+    def test_set_chunk_size(self):
+        downloader = TriggerDownloader(url=URL)
+        downloader.chunk_size = 1024
+        self.assertEqual(downloader.chunk_size, 1024)
+
+    def test_fail_set_chunk_size(self):
+        downloader = TriggerDownloader(url=URL)
+        with self.assertRaises(ValueError) as exc_info:
+            downloader.chunk_size = 1025
+
+        self.assertEqual(str(exc_info.exception), "1025 isnt a power of 2")
diff --git a/tests/test_006_stream_downloader.py b/tests/test_006_stream_downloader.py
new file mode 100644
index 00000000..6ca263d3
--- /dev/null
+++ b/tests/test_006_stream_downloader.py
@@ -0,0 +1,186 @@
+import io
+from unittest import TestCase
+from unittest.mock import patch, MagicMock, call
+import requests
+from src.utils.downloader.stream_downloader import StreamDownloader
+
+URL = "https://github.com/selfcustody/krux"
+
+MOCKED_FOUND_API = [
+    {"author": "test", "tag_name": "v0.0.1"},
+    {"author": "test", "tag_name": "v0.1.0"},
+    {"author": "test", "tag_name": "v1.0.0"},
+]
+
+
+class TestStreamDownloader(TestCase):
+
+    def test_init_on_data(self):
+        # fake a zip file to be downloaded
+        file = io.BytesIO()
+
+        # pylint: disable=line-too-long
+        file.write(
+            b"PK\x03\x04\x14\x00\x00\x00\x08\x00\x08`1Xb\x1f\x95Q\xb3\x00\x00\x00!\x04\x00\x00\x0e\x00\x1c\x00tests/krux.txtUT\t\x00\x03\x7f\xeb\xa7e\x7f\xeb\xa7eux\x0b\x00\x01\x04\xe8\x03\x00\x00\x04\xe8\x03\x00\x00\xad\x90K\x0e\xc2 \x14E\xe7\xae\x82\r\x94~\x8c\x03;s%\rE\xa8/\x94O(m\xd1\xd5k\xd5\xc4X\x13\x05\xe2\xf8\xdd\x93s\xf2\x0e\x96\x9e`b5B\xc2\x8e>\x9b\xaa-.\xf6\xb8\xc4\x170\x1b\x84@\xf1\x9e8P]\xfd~\xce\x85\xd3\xba\xcfzP\xa3G\xe8\xf7P\x12\x1a8\xcb\xca\"d9\x83\xc2\xcc\xb3\xefSI\xc0\x9bs#w\x83\x03*\xa6\x9c\x83\x953\xb1\x0c\xb77z\x80.\x9d\x8e#E\xab\xb5\xc3\x82\x1b\x11\xa6$\x12:\xdd\x80\x19\xe2\x9d/\xf4?\xd2\xe07=p\xc7]j\xf3\x82\xa65\xaf\xa5\xc1\xcd-$\xd4.Pl\xe7Z\x14\\x\xd4T\xc4'\xde\xa9\xd8\xc6\x0f\xd53\xf2\nPK\x01\x02\x1e\x03\x14\x00\x00\x00\x08\x00\x08`1Xb\x1f\x95Q\xb3\x00\x00\x00!\x04\x00\x00\x0e\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa4\x81\x00\x00\x00\x00tests/krux.txtUT\x05\x00\x03\x7f\xeb\xa7eux\x0b\x00\x01\x04\xe8\x03\x00\x00\x04\xe8\x03\x00\x00PK\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00T\x00\x00\x00\xfb\x00\x00\x00\x00\x00"
+        )
+
+        # mock a stream of bytes from fake zipfile
+        stream = [
+            [bytes(b) for b in file.read(i + 7)] for i in range(file.__sizeof__())
+        ]
+        s = StreamDownloader(url=URL)
+
+        # strange hack to allow it be
+        # registered as coveraged
+        s.on_data = MagicMock()
+        on_data = getattr(s, "on_data")
+
+        calls = []
+        for chunk in stream:
+            s.downloaded_len += len(chunk)
+            on_data(data=chunk)
+            calls.append(call()(data=[]))
+
+        on_data.assert_has_calls(calls, any_order=True)
+
+    @patch("src.utils.downloader.stream_downloader.requests")
+    def test_download_file_stream(self, mock_requests):
+        mock_response = MagicMock()
+        mock_response.status_code = 200
+        mock_response.headers = {"Content-Length": "210000"}
+        mock_response.json.return_value = MOCKED_FOUND_API
+        mock_requests.get.return_value = mock_response
+
+        sd = StreamDownloader(url=URL)
+        sd.download_file_stream(url="https://any.call/test.zip")
+
+        mock_requests.get.assert_called_once_with(
+            url="https://any.call/test.zip",
+            stream=True,
+            headers={
+                "Content-Disposition": "attachment filename=test.zip",
+                "Connection": "keep-alive",
+                "Cache-Control": "max-age=0",
+                "Accept-Encoding": "gzip, deflate, br",
+            },
+            timeout=30,
+        )
+        mock_requests.get.return_value.iter_content.assert_called_with(chunk_size=1024)
+
+    @patch("src.utils.downloader.stream_downloader.requests")
+    def test_download_file_stream_process_data(self, mock_requests):
+        # fake a zip file to be downloaded
+        file = io.BytesIO()
+
+        # pylint: disable=line-too-long
+        file.write(
+            b"PK\x03\x04\x14\x00\x00\x00\x08\x00\x08`1Xb\x1f\x95Q\xb3\x00\x00\x00!\x04\x00\x00\x0e\x00\x1c\x00tests/krux.txtUT\t\x00\x03\x7f\xeb\xa7e\x7f\xeb\xa7eux\x0b\x00\x01\x04\xe8\x03\x00\x00\x04\xe8\x03\x00\x00\xad\x90K\x0e\xc2 \x14E\xe7\xae\x82\r\x94~\x8c\x03;s%\rE\xa8/\x94O(m\xd1\xd5k\xd5\xc4X\x13\x05\xe2\xf8\xdd\x93s\xf2\x0e\x96\x9e`b5B\xc2\x8e>\x9b\xaa-.\xf6\xb8\xc4\x170\x1b\x84@\xf1\x9e8P]\xfd~\xce\x85\xd3\xba\xcfzP\xa3G\xe8\xf7P\x12\x1a8\xcb\xca\"d9\x83\xc2\xcc\xb3\xefSI\xc0\x9bs#w\x83\x03*\xa6\x9c\x83\x953\xb1\x0c\xb77z\x80.\x9d\x8e#E\xab\xb5\xc3\x82\x1b\x11\xa6$\x12:\xdd\x80\x19\xe2\x9d/\xf4?\xd2\xe07=p\xc7]j\xf3\x82\xa65\xaf\xa5\xc1\xcd-$\xd4.Pl\xe7Z\x14\\x\xd4T\xc4'\xde\xa9\xd8\xc6\x0f\xd53\xf2\nPK\x01\x02\x1e\x03\x14\x00\x00\x00\x08\x00\x08`1Xb\x1f\x95Q\xb3\x00\x00\x00!\x04\x00\x00\x0e\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa4\x81\x00\x00\x00\x00tests/krux.txtUT\x05\x00\x03\x7f\xeb\xa7eux\x0b\x00\x01\x04\xe8\x03\x00\x00\x04\xe8\x03\x00\x00PK\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00T\x00\x00\x00\xfb\x00\x00\x00\x00\x00"
+        )
+
+        # mock a stream of bytes from fake zipfile
+        stream = [
+            [bytes(b) for b in file.read(i + 7)] for i in range(file.__sizeof__())
+        ]
+
+        mock_response = MagicMock()
+        mock_response.status_code = 200
+        mock_response.headers = {"Content-Length": "210000"}
+        mock_response.json.return_value = MOCKED_FOUND_API
+        mock_response.iter_content.return_value = stream
+        mock_requests.get.return_value = mock_response
+
+        sd = StreamDownloader(url=URL)
+
+        # strange hack to allow it be
+        # registered as coveraged
+        sd.on_data = MagicMock()
+        on_data = getattr(sd, "on_data")
+
+        # download
+        sd.download_file_stream(url="https://any.call/test.zip")
+
+        mock_requests.get.assert_called_once_with(
+            url="https://any.call/test.zip",
+            stream=True,
+            headers={
+                "Content-Disposition": "attachment filename=test.zip",
+                "Connection": "keep-alive",
+                "Cache-Control": "max-age=0",
+                "Accept-Encoding": "gzip, deflate, br",
+            },
+            timeout=30,
+        )
+        mock_requests.get.return_value.iter_content.assert_called_with(chunk_size=1024)
+        assert len(on_data.mock_calls) > 0
+        on_data.assert_has_calls([call()(data=[])], any_order=True)
+
+    @patch("src.utils.downloader.stream_downloader.requests")
+    def test_fail_download_file_stream_no_content_len_header(self, mock_requests):
+        mock_response = MagicMock()
+        mock_response.status_code = 200
+        mock_response.headers = MagicMock()
+        mock_response.headers = {"No-Content-Length": "210000"}
+        mock_response.json.return_value = MOCKED_FOUND_API
+        mock_requests.get.return_value = mock_response
+
+        with self.assertRaises(RuntimeError) as exc_info:
+            sd = StreamDownloader(url=URL)
+            sd.download_file_stream(url="https://any.call/test.zip")
+
+        self.assertEqual(
+            str(exc_info.exception),
+            "Empty Content-Length response for https://any.call/test.zip",
+        )
+
+        mock_requests.get.assert_called_once_with(
+            url="https://any.call/test.zip",
+            stream=True,
+            headers={
+                "Content-Disposition": "attachment filename=test.zip",
+                "Connection": "keep-alive",
+                "Cache-Control": "max-age=0",
+                "Accept-Encoding": "gzip, deflate, br",
+            },
+            timeout=30,
+        )
+
+    @patch("src.utils.downloader.stream_downloader.requests")
+    def test_fail_server_error_download_file_stream(self, mock_requests):
+        mock_response = MagicMock(status_code=500)
+        mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError()
+        mock_requests.exceptions = requests.exceptions
+        mock_requests.get.return_value = mock_response
+
+        with self.assertRaises(RuntimeError) as exc_info:
+            sd = StreamDownloader(url=URL)
+            sd.download_file_stream(url="https://any.request/test.zip")
+
+        self.assertEqual(str(exc_info.exception), "HTTP error 500: None")
+
+    @patch("src.utils.downloader.stream_downloader.requests")
+    def test_fail_timeout_download_file_stream(self, mock_requests):
+        mock_response = MagicMock()
+        mock_response.raise_for_status.side_effect = requests.exceptions.Timeout()
+        mock_requests.exceptions = requests.exceptions
+        mock_requests.get.return_value = mock_response
+
+        with self.assertRaises(RuntimeError) as exc_info:
+            sd = StreamDownloader(url=URL)
+            sd.download_file_stream(url="https://any.request/test.zip")
+
+        self.assertEqual(str(exc_info.exception), "Download timeout error: None")
+
+    @patch("src.utils.downloader.stream_downloader.requests")
+    def test_fail_connection_download_file_stream(self, mock_requests):
+        mock_response = MagicMock()
+        mock_response.raise_for_status.side_effect = (
+            requests.exceptions.ConnectionError()
+        )
+        mock_requests.exceptions = requests.exceptions
+        mock_requests.get.return_value = mock_response
+
+        with self.assertRaises(RuntimeError) as exc_info:
+            sd = StreamDownloader(url=URL)
+            sd.download_file_stream(url="https://any.request/test.zip")
+
+        self.assertEqual(str(exc_info.exception), "Download connection error: None")
diff --git a/tests/test_007_asset_downloader.py b/tests/test_007_asset_downloader.py
new file mode 100644
index 00000000..1cf312e6
--- /dev/null
+++ b/tests/test_007_asset_downloader.py
@@ -0,0 +1,144 @@
+import io
+import sys
+from unittest import TestCase
+from unittest.mock import MagicMock, patch, mock_open
+from src.utils.downloader.asset_downloader import AssetDownloader
+from .shared_mocks import PropertyInstanceMock
+
+MOCKED_FOUND_API = [
+    {"author": "test", "tag_name": "v0.0.1"},
+    {"author": "test", "tag_name": "v0.1.0"},
+    {"author": "test", "tag_name": "v1.0.0"},
+]
+
+
+class TestAssetDownloader(TestCase):
+
+    @patch(
+        "src.utils.downloader.asset_downloader.AssetDownloader.destdir",
+        new_callable=PropertyInstanceMock,
+    )
+    @patch("tempfile.gettempdir")
+    def test_init_destdir(self, mock_gettempdir, mock_destdir):
+        mock_gettempdir.return_value = "/tmp/dir"
+
+        a = AssetDownloader(
+            url="https://github.com/selfcustody/krux/asset.zip",
+            destdir=mock_gettempdir(),
+            write_mode="w",
+        )
+
+        mock_destdir.assert_called_once_with(a, "/tmp/dir")
+
+    @patch(
+        "src.utils.downloader.asset_downloader.AssetDownloader.write_mode",
+        new_callable=PropertyInstanceMock,
+    )
+    @patch("tempfile.gettempdir")
+    def test_init_write_mode(self, mock_gettempdir, mock_mode):
+        mock_gettempdir.return_value = "/tmp/dir"
+
+        a = AssetDownloader(
+            url="https://github.com/selfcustody/krux/asset.zip",
+            destdir=mock_gettempdir(),
+            write_mode="w",
+        )
+
+        mock_mode.assert_called_once_with(a, "w")
+
+    @patch("tempfile.gettempdir")
+    def test_fail_init_write_mode(self, mock_gettempdir):
+        mock_gettempdir.return_value = "/tmp/dir"
+
+        with self.assertRaises(ValueError) as exc_info:
+            AssetDownloader(
+                url="https://github.com/selfcustody/krux/asset.zip",
+                destdir=mock_gettempdir(),
+                write_mode="r",
+            )
+
+        self.assertEqual(str(exc_info.exception), "Write Mode 'r' not supported")
+
+    @patch("builtins.open", new_callable=mock_open)
+    @patch("tempfile.gettempdir")
+    @patch("src.utils.downloader.stream_downloader.requests")
+    def test_download_wb(self, mock_requests, mock_gettempdir, open_mock):
+        if sys.platform in ("linux", "darwin"):
+            mock_gettempdir.return_value = "/tmp/dir"
+
+        if sys.platform == "win32":
+            mock_gettempdir.return_value = "C:\\tmp\\dir"
+
+        file = io.BytesIO()
+
+        # pylint: disable=line-too-long
+        file.write(
+            b"PK\x03\x04\x14\x00\x00\x00\x08\x00\x08`1Xb\x1f\x95Q\xb3\x00\x00\x00!\x04\x00\x00\x0e\x00\x1c\x00tests/krux.txtUT\t\x00\x03\x7f\xeb\xa7e\x7f\xeb\xa7eux\x0b\x00\x01\x04\xe8\x03\x00\x00\x04\xe8\x03\x00\x00\xad\x90K\x0e\xc2 \x14E\xe7\xae\x82\r\x94~\x8c\x03;s%\rE\xa8/\x94O(m\xd1\xd5k\xd5\xc4X\x13\x05\xe2\xf8\xdd\x93s\xf2\x0e\x96\x9e`b5B\xc2\x8e>\x9b\xaa-.\xf6\xb8\xc4\x170\x1b\x84@\xf1\x9e8P]\xfd~\xce\x85\xd3\xba\xcfzP\xa3G\xe8\xf7P\x12\x1a8\xcb\xca\"d9\x83\xc2\xcc\xb3\xefSI\xc0\x9bs#w\x83\x03*\xa6\x9c\x83\x953\xb1\x0c\xb77z\x80.\x9d\x8e#E\xab\xb5\xc3\x82\x1b\x11\xa6$\x12:\xdd\x80\x19\xe2\x9d/\xf4?\xd2\xe07=p\xc7]j\xf3\x82\xa65\xaf\xa5\xc1\xcd-$\xd4.Pl\xe7Z\x14\\x\xd4T\xc4'\xde\xa9\xd8\xc6\x0f\xd53\xf2\nPK\x01\x02\x1e\x03\x14\x00\x00\x00\x08\x00\x08`1Xb\x1f\x95Q\xb3\x00\x00\x00!\x04\x00\x00\x0e\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa4\x81\x00\x00\x00\x00tests/krux.txtUT\x05\x00\x03\x7f\xeb\xa7eux\x0b\x00\x01\x04\xe8\x03\x00\x00\x04\xe8\x03\x00\x00PK\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00T\x00\x00\x00\xfb\x00\x00\x00\x00\x00"
+        )
+
+        stream = [bytes(b) for b in file.read(8)]
+
+        mock_response = MagicMock()
+        mock_response.status_code = 200
+        mock_response.headers = {"Content-Length": "210000"}
+        mock_response.json.return_value = MOCKED_FOUND_API
+        mock_response.iter_content.return_value = stream
+        mock_requests.get.return_value = mock_response
+
+        a = AssetDownloader(
+            url="https://github.com/selfcustody/krux/asset.zip",
+            destdir=mock_gettempdir(),
+            write_mode="wb",
+        )
+
+        mock_on_data = MagicMock()
+        a.download(on_data=mock_on_data)
+
+        if sys.platform in ("linux", "darwin"):
+            open_mock.assert_called_once_with("/tmp/dir/asset.zip", "wb")
+
+        if sys.platform == "win32":
+            open_mock.assert_called_once_with("C:\\tmp\\dir\\asset.zip", "wb")
+
+    @patch("builtins.open", new_callable=mock_open)
+    @patch("tempfile.gettempdir")
+    @patch("src.utils.downloader.stream_downloader.requests")
+    def test_download_w(self, mock_requests, mock_gettempdir, open_mock):
+        if sys.platform in ("linux", "darwin"):
+            mock_gettempdir.return_value = "/tmp/dir"
+
+        if sys.platform == "win32":
+            mock_gettempdir.return_value = "C:\\tmp\\dir"
+
+        file = io.BytesIO()
+
+        # pylint: disable=line-too-long
+        file.write(
+            b"PK\x03\x04\x14\x00\x00\x00\x08\x00\x08`1Xb\x1f\x95Q\xb3\x00\x00\x00!\x04\x00\x00\x0e\x00\x1c\x00tests/krux.txtUT\t\x00\x03\x7f\xeb\xa7e\x7f\xeb\xa7eux\x0b\x00\x01\x04\xe8\x03\x00\x00\x04\xe8\x03\x00\x00\xad\x90K\x0e\xc2 \x14E\xe7\xae\x82\r\x94~\x8c\x03;s%\rE\xa8/\x94O(m\xd1\xd5k\xd5\xc4X\x13\x05\xe2\xf8\xdd\x93s\xf2\x0e\x96\x9e`b5B\xc2\x8e>\x9b\xaa-.\xf6\xb8\xc4\x170\x1b\x84@\xf1\x9e8P]\xfd~\xce\x85\xd3\xba\xcfzP\xa3G\xe8\xf7P\x12\x1a8\xcb\xca\"d9\x83\xc2\xcc\xb3\xefSI\xc0\x9bs#w\x83\x03*\xa6\x9c\x83\x953\xb1\x0c\xb77z\x80.\x9d\x8e#E\xab\xb5\xc3\x82\x1b\x11\xa6$\x12:\xdd\x80\x19\xe2\x9d/\xf4?\xd2\xe07=p\xc7]j\xf3\x82\xa65\xaf\xa5\xc1\xcd-$\xd4.Pl\xe7Z\x14\\x\xd4T\xc4'\xde\xa9\xd8\xc6\x0f\xd53\xf2\nPK\x01\x02\x1e\x03\x14\x00\x00\x00\x08\x00\x08`1Xb\x1f\x95Q\xb3\x00\x00\x00!\x04\x00\x00\x0e\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa4\x81\x00\x00\x00\x00tests/krux.txtUT\x05\x00\x03\x7f\xeb\xa7eux\x0b\x00\x01\x04\xe8\x03\x00\x00\x04\xe8\x03\x00\x00PK\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00T\x00\x00\x00\xfb\x00\x00\x00\x00\x00"
+        )
+
+        stream = [bytes(b) for b in file.read(8)]
+
+        mock_response = MagicMock()
+        mock_response.status_code = 200
+        mock_response.iter_content.return_value = stream
+        mock_requests.get.return_value = mock_response
+
+        a = AssetDownloader(
+            url="https://github.com/selfcustody/krux/asset.txt",
+            destdir=mock_gettempdir(),
+            write_mode="w",
+        )
+
+        mock_on_data = MagicMock()
+        a.download(on_data=mock_on_data)
+
+        if sys.platform in ("linux", "darwin"):
+            open_mock.assert_called_once_with(
+                "/tmp/dir/asset.txt", "w", encoding="utf8"
+            )
+
+        if sys.platform == "win32":
+            open_mock.assert_called_once_with(
+                "C:\\tmp\\dir\\asset.txt", "w", encoding="utf8"
+            )
diff --git a/tests/test_008_zip_downloader.py b/tests/test_008_zip_downloader.py
new file mode 100644
index 00000000..c396c4cb
--- /dev/null
+++ b/tests/test_008_zip_downloader.py
@@ -0,0 +1,30 @@
+from unittest import TestCase
+from unittest.mock import patch
+from src.utils.downloader import ZipDownloader
+
+
+class TestZipDownloader(TestCase):
+
+    @patch("tempfile.gettempdir")
+    def test_init_url(self, mock_gettempdir):
+        mock_gettempdir.return_value = "/tmp/dir"
+
+        z = ZipDownloader(version="v0.0.1")
+        self.assertEqual(
+            z.url,
+            "https://github.com/selfcustody/krux/releases/download/v0.0.1/krux-v0.0.1.zip",
+        )
+
+    @patch("tempfile.gettempdir")
+    def test_init_destdir(self, mock_gettempdir):
+        mock_gettempdir.return_value = "/tmp/dir"
+
+        z = ZipDownloader(version="v0.0.1", destdir=mock_gettempdir())
+        self.assertEqual(z.destdir, "/tmp/dir")
+
+    @patch("tempfile.gettempdir")
+    def test_init_write_mode(self, mock_gettempdir):
+        mock_gettempdir.return_value = "/tmp/dir"
+
+        z = ZipDownloader(version="v0.0.1", destdir=mock_gettempdir())
+        self.assertEqual(z.write_mode, "wb")
diff --git a/tests/test_009_sha256_downloader.py b/tests/test_009_sha256_downloader.py
new file mode 100644
index 00000000..8950f2f7
--- /dev/null
+++ b/tests/test_009_sha256_downloader.py
@@ -0,0 +1,30 @@
+from unittest import TestCase
+from unittest.mock import patch
+from src.utils.downloader import Sha256Downloader
+
+
+URL = "https://github.com/selfcustody/krux/releases/download/v0.0.1/krux-v0.0.1.zip.sha256.txt"
+
+
+class TestSha256Downloader(TestCase):
+
+    @patch("tempfile.gettempdir")
+    def test_init_url(self, mock_gettempdir):
+        mock_gettempdir.return_value = "/tmp/dir"
+
+        z = Sha256Downloader(version="v0.0.1")
+        self.assertEqual(z.url, URL)
+
+    @patch("tempfile.gettempdir")
+    def test_init_destdir(self, mock_gettempdir):
+        mock_gettempdir.return_value = "/tmp/dir"
+
+        z = Sha256Downloader(version="v0.0.1", destdir=mock_gettempdir())
+        self.assertEqual(z.destdir, "/tmp/dir")
+
+    @patch("tempfile.gettempdir")
+    def test_init_write_mode(self, mock_gettempdir):
+        mock_gettempdir.return_value = "/tmp/dir"
+
+        z = Sha256Downloader(version="v0.0.1", destdir=mock_gettempdir())
+        self.assertEqual(z.write_mode, "w")
diff --git a/tests/test_010_pem_downloader.py b/tests/test_010_pem_downloader.py
new file mode 100644
index 00000000..e99f187b
--- /dev/null
+++ b/tests/test_010_pem_downloader.py
@@ -0,0 +1,30 @@
+from unittest import TestCase
+from unittest.mock import patch
+from src.utils.downloader import PemDownloader
+
+
+URL = "https://raw.githubusercontent.com/selfcustody/krux/main/selfcustody.pem"
+
+
+class TestPemDownloader(TestCase):
+
+    @patch("tempfile.gettempdir")
+    def test_init_url(self, mock_gettempdir):
+        mock_gettempdir.return_value = "/tmp/dir"
+
+        z = PemDownloader()
+        self.assertEqual(z.url, URL)
+
+    @patch("tempfile.gettempdir")
+    def test_init_destdir(self, mock_gettempdir):
+        mock_gettempdir.return_value = "/tmp/dir"
+
+        z = PemDownloader(destdir=mock_gettempdir())
+        self.assertEqual(z.destdir, "/tmp/dir")
+
+    @patch("tempfile.gettempdir")
+    def test_init_write_mode(self, mock_gettempdir):
+        mock_gettempdir.return_value = "/tmp/dir"
+
+        z = PemDownloader(destdir=mock_gettempdir())
+        self.assertEqual(z.write_mode, "w")
diff --git a/tests/test_011_sig_downloader.py b/tests/test_011_sig_downloader.py
new file mode 100644
index 00000000..fe7a4dfb
--- /dev/null
+++ b/tests/test_011_sig_downloader.py
@@ -0,0 +1,30 @@
+from unittest import TestCase
+from unittest.mock import patch
+from src.utils.downloader import SigDownloader
+
+
+URL = "https://github.com/selfcustody/krux/releases/download/v0.0.1/krux-v0.0.1.zip.sig"
+
+
+class TestSigDownloader(TestCase):
+
+    @patch("tempfile.gettempdir")
+    def test_init_url(self, mock_gettempdir):
+        mock_gettempdir.return_value = "/tmp/dir"
+
+        z = SigDownloader(version="v0.0.1")
+        self.assertEqual(z.url, URL)
+
+    @patch("tempfile.gettempdir")
+    def test_init_destdir(self, mock_gettempdir):
+        mock_gettempdir.return_value = "/tmp/dir"
+
+        z = SigDownloader(version="v0.0.1", destdir=mock_gettempdir())
+        self.assertEqual(z.destdir, "/tmp/dir")
+
+    @patch("tempfile.gettempdir")
+    def test_init_write_mode(self, mock_gettempdir):
+        mock_gettempdir.return_value = "/tmp/dir"
+
+        z = SigDownloader(version="v0.0.1", destdir=mock_gettempdir())
+        self.assertEqual(z.write_mode, "wb")
diff --git a/tests/test_012_beta_downloader.py b/tests/test_012_beta_downloader.py
new file mode 100644
index 00000000..56a7fcf6
--- /dev/null
+++ b/tests/test_012_beta_downloader.py
@@ -0,0 +1,90 @@
+from unittest import TestCase
+from unittest.mock import patch, call
+from src.utils.downloader import BetaDownloader
+from .shared_mocks import PropertyInstanceMock
+
+
+BASE_URL = "https://raw.githubusercontent.com/odudex/krux_binaries/main"
+
+
+class TestBetaDownloader(TestCase):
+
+    @patch(
+        "src.utils.downloader.BetaDownloader.device", new_callable=PropertyInstanceMock
+    )
+    @patch(
+        "src.utils.downloader.BetaDownloader.binary_type",
+        new_callable=PropertyInstanceMock,
+    )
+    @patch("tempfile.gettempdir")
+    def test_calls_init(self, mock_gettempdir, mock_binary_type, mock_device):
+        mock_gettempdir.return_value = "/tmp/dir"
+        for device in BetaDownloader.VALID_DEVICES:
+            for _bin in BetaDownloader.VALID_BINARY_TYPES:
+                b = BetaDownloader(
+                    device=device, binary_type=_bin, destdir=mock_gettempdir()
+                )
+                mock_device.assert_has_calls([call(b, device)])
+                mock_binary_type.assert_has_calls([call(b, _bin)])
+
+    @patch("tempfile.gettempdir")
+    def test_init_url(self, mock_gettempdir):
+        mock_gettempdir.return_value = "/tmp/dir"
+        for device in BetaDownloader.VALID_DEVICES:
+            for _bin in BetaDownloader.VALID_BINARY_TYPES:
+                mock_url = f"{BASE_URL}/maixpy_{device}/{_bin}"
+                b = BetaDownloader(
+                    device=device, binary_type=_bin, destdir=mock_gettempdir()
+                )
+                self.assertEqual(b.url, mock_url)
+
+    @patch("tempfile.gettempdir")
+    def test_fail_init_device(self, mock_gettempdir):
+        mock_gettempdir.return_value = "/tmp/dir"
+
+        with self.assertRaises(ValueError) as exc_info:
+            BetaDownloader(
+                device="lilygo", binary_type="firmware", destdir=mock_gettempdir()
+            )
+
+        self.assertEqual(str(exc_info.exception), "Invalid device lilygo")
+
+    @patch("tempfile.gettempdir")
+    def test_fail_binary_type(self, mock_gettempdir):
+        mock_gettempdir.return_value = "/tmp/dir"
+
+        with self.assertRaises(ValueError) as exc_info:
+            BetaDownloader(
+                device="amigo", binary_type="esp32", destdir=mock_gettempdir()
+            )
+
+        self.assertEqual(str(exc_info.exception), "Invalid binary_type esp32")
+
+    @patch("tempfile.gettempdir")
+    def test_init_destdir(self, mock_gettempdir):
+        mock_gettempdir.return_value = "/tmp/dir"
+        for device in BetaDownloader.VALID_DEVICES:
+            for _bin in BetaDownloader.VALID_BINARY_TYPES:
+                b = BetaDownloader(
+                    device=device, binary_type=_bin, destdir=mock_gettempdir()
+                )
+                self.assertEqual(b.destdir, "/tmp/dir")
+
+    @patch("tempfile.gettempdir")
+    def test_init_write_mode(self, mock_gettempdir):
+        mock_gettempdir.return_value = "/tmp/dir"
+        for device in BetaDownloader.VALID_DEVICES:
+            for _bin in BetaDownloader.VALID_BINARY_TYPES:
+                b = BetaDownloader(
+                    device=device, binary_type=_bin, destdir=mock_gettempdir()
+                )
+                self.assertEqual(b.write_mode, "wb")
+
+    @patch("tempfile.gettempdir")
+    def test_set_properties(self, mock_gettempdir):
+        mock_gettempdir.return_value = "/tmp/dir"
+        b = BetaDownloader(
+            device="m5stickv", binary_type="kboot.kfpkg", destdir=mock_gettempdir()
+        )
+        self.assertEqual(b.device, "m5stickv")
+        self.assertEqual(b.binary_type, "kboot.kfpkg")
diff --git a/tests/test_013_base_verifyer.py b/tests/test_013_base_verifyer.py
new file mode 100644
index 00000000..1c8dfa8d
--- /dev/null
+++ b/tests/test_013_base_verifyer.py
@@ -0,0 +1,61 @@
+from unittest import TestCase
+from unittest.mock import patch
+from src.utils.verifyer.base_verifyer import BaseVerifyer
+from .shared_mocks import PropertyInstanceMock
+
+
+class TestBaseVerifyerDownloader(TestCase):
+
+    @patch(
+        "src.utils.verifyer.base_verifyer.BaseVerifyer.filename",
+        new_callable=PropertyInstanceMock,
+    )
+    def test_init_filename_r(self, mock_filename):
+        b = BaseVerifyer(filename="mockfile.txt", read_mode="r")
+        mock_filename.assert_called_once_with(b, "mockfile.txt")
+
+    def test_get_filename_r(self):
+        b = BaseVerifyer(filename="mockfile.txt", read_mode="r")
+        self.assertEqual(b.filename, "mockfile.txt")
+
+    @patch(
+        "src.utils.verifyer.base_verifyer.BaseVerifyer.read_mode",
+        new_callable=PropertyInstanceMock,
+    )
+    def test_init_read_mode_r(self, mock_read_mode):
+        b = BaseVerifyer(filename="mockfile.txt", read_mode="r")
+        mock_read_mode.assert_called_once_with(b, "r")
+
+    @patch(
+        "src.utils.verifyer.base_verifyer.BaseVerifyer.read_mode",
+        new_callable=PropertyInstanceMock,
+    )
+    def test_init_read_mode_rb(self, mock_read_mode):
+        b = BaseVerifyer(filename="mockfile.txt", read_mode="rb")
+        mock_read_mode.assert_called_once_with(b, "rb")
+
+    def test_get_read_mode_r(self):
+        b = BaseVerifyer(filename="mockfile.txt", read_mode="r")
+        self.assertEqual(b.read_mode, "r")
+
+    def test_get_read_mode_rb(self):
+        b = BaseVerifyer(filename="mockfile.txt", read_mode="rb")
+        self.assertEqual(b.read_mode, "rb")
+
+    def test_fail_init_read_mode(self):
+        with self.assertRaises(ValueError) as exc_info:
+            BaseVerifyer(filename="mockfile.txt", read_mode="w")
+
+        self.assertEqual(str(exc_info.exception), "Invalid read_mode: w")
+
+    @patch(
+        "src.utils.verifyer.base_verifyer.BaseVerifyer.data",
+        new_callable=PropertyInstanceMock,
+    )
+    def test_init_data(self, mock_data):
+        b = BaseVerifyer(filename="mockfile.txt", read_mode="rb")
+        mock_data.assert_called_once_with(b, None)
+
+    def test_get_data(self):
+        b = BaseVerifyer(filename="mockfile.txt", read_mode="rb")
+        self.assertEqual(b.data, None)
diff --git a/tests/test_014_check_verifyer.py b/tests/test_014_check_verifyer.py
new file mode 100644
index 00000000..73f21cd1
--- /dev/null
+++ b/tests/test_014_check_verifyer.py
@@ -0,0 +1,59 @@
+import io
+from unittest import TestCase
+from unittest.mock import patch, mock_open
+from src.utils.verifyer.check_verifyer import CheckVerifyer
+
+mock_file = io.BytesIO()
+
+# pylint: disable=line-too-long
+mock_file.write(
+    b"PK\x03\x04\x14\x00\x00\x00\x08\x00\x08`1Xb\x1f\x95Q\xb3\x00\x00\x00!\x04\x00\x00\x0e\x00\x1c\x00tests/krux.txtUT\t\x00\x03\x7f\xeb\xa7e\x7f\xeb\xa7eux\x0b\x00\x01\x04\xe8\x03\x00\x00\x04\xe8\x03\x00\x00\xad\x90K\x0e\xc2 \x14E\xe7\xae\x82\r\x94~\x8c\x03;s%\rE\xa8/\x94O(m\xd1\xd5k\xd5\xc4X\x13\x05\xe2\xf8\xdd\x93s\xf2\x0e\x96\x9e`b5B\xc2\x8e>\x9b\xaa-.\xf6\xb8\xc4\x170\x1b\x84@\xf1\x9e8P]\xfd~\xce\x85\xd3\xba\xcfzP\xa3G\xe8\xf7P\x12\x1a8\xcb\xca\"d9\x83\xc2\xcc\xb3\xefSI\xc0\x9bs#w\x83\x03*\xa6\x9c\x83\x953\xb1\x0c\xb77z\x80.\x9d\x8e#E\xab\xb5\xc3\x82\x1b\x11\xa6$\x12:\xdd\x80\x19\xe2\x9d/\xf4?\xd2\xe07=p\xc7]j\xf3\x82\xa65\xaf\xa5\xc1\xcd-$\xd4.Pl\xe7Z\x14\\x\xd4T\xc4'\xde\xa9\xd8\xc6\x0f\xd53\xf2\nPK\x01\x02\x1e\x03\x14\x00\x00\x00\x08\x00\x08`1Xb\x1f\x95Q\xb3\x00\x00\x00!\x04\x00\x00\x0e\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa4\x81\x00\x00\x00\x00tests/krux.txtUT\x05\x00\x03\x7f\xeb\xa7eux\x0b\x00\x01\x04\xe8\x03\x00\x00\x04\xe8\x03\x00\x00PK\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00T\x00\x00\x00\xfb\x00\x00\x00\x00\x00"
+)
+
+
+class TestCheckVerifyerDownloader(TestCase):
+
+    @patch("re.findall", return_value=["test.mock"])
+    @patch("os.path.exists", return_value=True)
+    def test_init(self, mock_exists, mock_findall):
+        c = CheckVerifyer(filename="test.mock", read_mode="r", regexp=r".*mock")
+        self.assertEqual(c.filename, "test.mock")
+        mock_findall.assert_called_once_with(r".*mock", "test.mock")
+        mock_exists.assert_called_once_with("test.mock")
+
+    @patch("re.findall", return_value=[])
+    def test_fail_init_re(self, mock_findall):
+        with self.assertRaises(ValueError) as exc_info:
+            CheckVerifyer(filename="test.mock", read_mode="r", regexp=r".*notmock")
+
+        mock_findall.assert_called_once_with(r".*notmock", "test.mock")
+        self.assertEqual(
+            str(exc_info.exception),
+            "Invalid file: test.mock do not assert with .*notmock",
+        )
+
+    @patch("os.path.exists", return_value=False)
+    def test_fail_init_exists(self, mock_exists):
+        with self.assertRaises(ValueError) as exc_info:
+            CheckVerifyer(filename="test.mock", read_mode="r", regexp=r".*\.mock")
+
+        mock_exists.assert_called_once_with("test.mock")
+        self.assertEqual(str(exc_info.exception), "File test.mock do not exist")
+
+    @patch("os.path.exists", return_value=True)
+    @patch("builtins.open", new_callable=mock_open, read_data="Hello World!")
+    def test_load_r(self, open_mock, mock_exists):
+        c = CheckVerifyer(filename="test.mock", read_mode="r", regexp=r".*\.mock")
+        c.load()
+        mock_exists.assert_called_once_with("test.mock")
+        open_mock.assert_called_once_with("test.mock", "r", encoding="utf8")
+        self.assertEqual(c.data, "Hello World!")
+
+    @patch("os.path.exists", return_value=True)
+    @patch("builtins.open", new_callable=mock_open, read_data=mock_file.read())
+    def test_load_rb(self, open_mock, mock_exists):
+        c = CheckVerifyer(filename="test.mock", read_mode="rb", regexp=r".*\.mock")
+        c.load()
+        mock_exists.assert_called_once_with("test.mock")
+        open_mock.assert_called_once_with("test.mock", "rb")
+        self.assertIsInstance(c.data, bytes)
diff --git a/tests/test_015_sha256_check_verifyer.py b/tests/test_015_sha256_check_verifyer.py
new file mode 100644
index 00000000..8ee79712
--- /dev/null
+++ b/tests/test_015_sha256_check_verifyer.py
@@ -0,0 +1,37 @@
+from unittest import TestCase
+from unittest.mock import patch, mock_open
+from src.utils.verifyer.sha256_check_verifyer import Sha256CheckVerifyer
+
+MOCK_SHA = "64675a1afffaa7b2dcf85283e664d662e4b8741cf0638df873dafee3b6cf749b"
+
+
+class TestSha256CheckVerifyerDownloader(TestCase):
+
+    @patch("os.path.exists", return_value=True)
+    def test_init(self, mock_exists):
+        c = Sha256CheckVerifyer(filename="test.mock.sha256.txt")
+        self.assertEqual(c.filename, "test.mock.sha256.txt")
+        self.assertEqual(c.read_mode, "r")
+        mock_exists.assert_called_once_with("test.mock.sha256.txt")
+
+    @patch("os.path.exists", return_value=True)
+    def test_fail_init(self, mock_exists):
+        with self.assertRaises(ValueError) as exc_info:
+            Sha256CheckVerifyer(filename="test.mock.txt")
+            mock_exists.assert_called_once_with("test.mock.txt")
+
+        self.assertEqual(
+            str(exc_info.exception),
+            "Invalid file: test.mock.txt do not assert with .*\\.sha256\\.txt",
+        )
+
+    @patch("os.path.exists", return_value=True)
+    @patch("builtins.open", new_callable=mock_open, read_data=MOCK_SHA)
+    def test_load(self, open_mock, mock_exists):
+        sha = Sha256CheckVerifyer(filename="test.mock.sha256.txt")
+        sha.load()
+        mock_exists.assert_called_once_with("test.mock.sha256.txt")
+        open_mock.assert_called_once_with("test.mock.sha256.txt", "r", encoding="utf8")
+        self.assertEqual(
+            sha.data, "64675a1afffaa7b2dcf85283e664d662e4b8741cf0638df873dafee3b6cf749b"
+        )
diff --git a/tests/test_016_pem_check_verifyer.py b/tests/test_016_pem_check_verifyer.py
new file mode 100644
index 00000000..af7da4d5
--- /dev/null
+++ b/tests/test_016_pem_check_verifyer.py
@@ -0,0 +1,44 @@
+from unittest import TestCase
+from unittest.mock import patch, mock_open
+from src.utils.verifyer.pem_check_verifyer import PemCheckVerifyer
+
+MOCK_PEM = """-----BEGIN PUBLIC KEY-----
+MDYwEAYHKoZIzj0CAQYFK4EEAAoDIgADM56IMVfkWJHmHKnfTNO7iV7zLUdbjnk1
+WeoQo2dmaJs=
+-----END PUBLIC KEY-----"""
+
+
+class TestPemCheckVerifyerDownloader(TestCase):
+
+    @patch("os.path.exists", return_value=True)
+    def test_init(self, mock_exists):
+        p = PemCheckVerifyer(filename="test.mock.pem")
+        self.assertEqual(p.filename, "test.mock.pem")
+        self.assertEqual(p.read_mode, "rb")
+        mock_exists.assert_called_once_with("test.mock.pem")
+
+    @patch("os.path.exists", return_value=True)
+    def test_fail_init(self, mock_exists):
+        with self.assertRaises(ValueError) as exc_info:
+            PemCheckVerifyer(filename="test.mock.txt")
+            mock_exists.assert_called_once_with("test.mock.txt")
+
+        self.assertEqual(
+            str(exc_info.exception),
+            "Invalid file: test.mock.txt do not assert with .*\\.pem",
+        )
+
+    @patch("os.path.exists", return_value=True)
+    @patch("builtins.open", new_callable=mock_open, read_data=MOCK_PEM)
+    def test_load(self, open_mock, mock_exists):
+        p = PemCheckVerifyer(filename="test.mock.pem")
+        p.load()
+        mock_exists.assert_called_once_with("test.mock.pem")
+        open_mock.assert_called_once_with("test.mock.pem", "rb")
+        self.assertEqual(
+            p.data,
+            """-----BEGIN PUBLIC KEY-----
+MDYwEAYHKoZIzj0CAQYFK4EEAAoDIgADM56IMVfkWJHmHKnfTNO7iV7zLUdbjnk1
+WeoQo2dmaJs=
+-----END PUBLIC KEY-----""",
+        )
diff --git a/tests/test_017_sig_check_verifyer.py b/tests/test_017_sig_check_verifyer.py
new file mode 100644
index 00000000..2bf57af0
--- /dev/null
+++ b/tests/test_017_sig_check_verifyer.py
@@ -0,0 +1,39 @@
+from unittest import TestCase
+from unittest.mock import patch, mock_open
+from src.utils.verifyer.sig_check_verifyer import SigCheckVerifyer
+
+# pylint: disable=line-too-long
+MOCK_SIG = b"0D\x02 8\x03&\xf5T\xa6\x08 #\xc0\x01\x02\xe5\xcb\xfe\xdd\xb3.\x86\xb6{W\x14\x9c\x04o\xf7m\xe5\x86T\xeb\x02 =\xb8\x9a\x83\x16\x1a\xe1R&\x14F\xab\x84\xceq\xcd\x1b\xacd\x15uI\xc4l\xd7X\x91\xdbq\xa6\xf8\xc0"
+
+
+class TestPemCheckVerifyerDownloader(TestCase):
+
+    @patch("os.path.exists", return_value=True)
+    def test_init(self, mock_exists):
+        s = SigCheckVerifyer(filename="test.mock.sig")
+        self.assertEqual(s.filename, "test.mock.sig")
+        self.assertEqual(s.read_mode, "rb")
+        mock_exists.assert_called_once_with("test.mock.sig")
+
+    @patch("os.path.exists", return_value=True)
+    def test_fail_init(self, mock_exists):
+        with self.assertRaises(ValueError) as exc_info:
+            SigCheckVerifyer(filename="test.mock.txt")
+            mock_exists.assert_called_once_with("test.mock.txt")
+
+        self.assertEqual(
+            str(exc_info.exception),
+            "Invalid file: test.mock.txt do not assert with .*\\.sig",
+        )
+
+    @patch("os.path.exists", return_value=True)
+    @patch("builtins.open", new_callable=mock_open, read_data=MOCK_SIG)
+    def test_load(self, open_mock, mock_exists):
+        s = SigCheckVerifyer(filename="test.mock.sig")
+        s.load()
+        mock_exists.assert_called_once_with("test.mock.sig")
+        open_mock.assert_called_once_with("test.mock.sig", "rb")
+        self.assertEqual(
+            s.data,
+            b"0D\x02 8\x03&\xf5T\xa6\x08 #\xc0\x01\x02\xe5\xcb\xfe\xdd\xb3.\x86\xb6{W\x14\x9c\x04o\xf7m\xe5\x86T\xeb\x02 =\xb8\x9a\x83\x16\x1a\xe1R&\x14F\xab\x84\xceq\xcd\x1b\xacd\x15uI\xc4l\xd7X\x91\xdbq\xa6\xf8\xc0",
+        )
diff --git a/tests/test_018_sha256_verifyer.py b/tests/test_018_sha256_verifyer.py
new file mode 100644
index 00000000..f2f61657
--- /dev/null
+++ b/tests/test_018_sha256_verifyer.py
@@ -0,0 +1,49 @@
+from unittest import TestCase
+from unittest.mock import patch, mock_open
+from src.utils.verifyer.sha256_verifyer import Sha256Verifyer
+
+MOCK_SHA = "4ab12c3cc56b2641e7b216666186558cf40a36e76947edfd1b37cc1b190255ac"
+
+# pylint: disable=line-too-long
+MOCK_ZIP = b'PK\x03\x04\x14\x00\x00\x00\x08\x00Aj>Xf\x088\xde\xec\x02\x00\x00H\x07\x00\x00\t\x00\x1c\x00README.mdUT\t\x00\x03:!\xb9ex\xe3\xbbeux\x0b\x00\x01\x04\xe8\x03\x00\x00\x04\xe8\x03\x00\x00\xadT\xc1\x8e\xd30\x10\xbd\xe7+\x06\xf5B\xa5\xa6\xdeE\x0b\x87\x95*\x04\x8b\x80\xd5\xb2\x12\x12\xcb\x01UHu\x9cib\xd5\x89\x83\xed\xa4\x9b\x1b\x1f\xc8G1\xb6\x13\xb5\xddv\x11\x8b8$J\xc6\x9e\xf7\xde<\xcfx\x027\xa6\xbd\x87\xeb\xda:\xae\x14\x9a$Y>[\xbem\xa5\xca\xa1\xe2\xb2\x86\xcc\xf0Z\x94\xdf\x9f\x97\xce5\xf6\x92\xb1B\xba\xb2\xcd\xe6BW\xcc\xa2Z\x8b\xd6:\x9d\xf7lC \xa9\x1cA\x18\x17N\xea\xda\xb2\xad6\x9b\xb5\xd2[\xcb2\x0f9\xef+\xc52\x9e\x178\xb7]\xf1:b/<\xcf\xf4?2LC\rB\xe7(t\xb7\xc3\x1d\x02s\xa9YQ\xb2\x1f\xca\xe4\x0f1\x9dAd\x1b\xd9\xf5\xac0\xbc)\xf7\x94:\xbd\xc1zq\xf3\xee\xe2\xfc\xe3\x8b\xb3\xdbo_\xa6O\x80%9\x87\x16\x03\x97\x95\x05\xa7!C\xe0\xf0\xe1\xeb5d\xdcbN\x11\xadB\xd8\x172K\xd6\x8a\xdb\x12x\x9dC\x8eY[\xc0\xd2\xa3\xfc\x95M\xc4\xf8\xc6\x82tP\xeb\xed\x0c\\\x89P`\x8d\x86;"\xe1M\xa3\xa4\xe0\xde<\xc0{\x14\xad\xc3\x19l\tK\xb7\x0e\\\xdf\xc8\xba \xce>!\xdc\xcasS\x0b84\x95\xac\xb9\nH\xcb(\xcb\x7f\xae\xa5\xa9\xb6\xdc \xe8\x9aT\xfbH\x8e\x9d\x14\xb8\xd3\xb8\'l>\xe8%\x9b\xbcDV\xa0s\xc4\x95\x92\'\x86t\xb1\xc1.\n\xb1I\xa0H\t0\x1d)RO\x11"\x91b:O\x92d2\x19=M\x92[\xbeA\xb0-\x89\xe9u\x0b%\xef\x10\x9a\x9e\x8a\xaa/\x93d\xb5Z\x91\xc1e\x12\x03\x90\xa6\x1d\x1aK\xf5\xfb\x85\x93\x99\xabF\xa33\xfd\n\xc63\xcc\x07\x94_?\xf7q*hds?n\x82\x98\xe4\xf7\x11\xea\x95\xd25\x06O\x0c6\xdaJ\xa7\xe3R\x10BV\x80\x08\x1b\xd24\xce\x00\xf8\xae\x83\x13G{\xd8J\xde\xc4\xa8\xfaz$\x8dZrl\xb0\xce\xb1\x16\x12\xed\xa1\xd6 j\xd4\x18s\xc9\xb7\xcf:\x8a\x1b4\x87\x06Mw\r\xdaZ\xb4\xc1\x85\x158n7t\x13\xd4\xbc\xa0\xf8Z\x87\xa7\xe2\xe1\xecf@\xc7\x15>\x12\x87\xd6\xd9\xd0\xab4\n\xd4i46p\xa7\xc1"u8\xa9\xe4\x1d\x97\x8ag\n\x03\x9c\x9d\x81i\x0f\x0e&\x8a\xa4\xa0\x17\x14E\x92\xca\t\xbc\x0f\\\xe0G\xec\xb1\xdd\x83\x9e\x98tW\xd2ni\xc1\xca\x8a\xf8\x0c\r\xd3i\x96Lq\xb1\x819\xb3F\x9c\n\x87jF\xaf&\xf0\x89\xaa\xa4\x89Z;\x1c\xaa\x8f\xa0\x0f\x85x3\x8ed\xec\xab8H\xe8\xfd\xf6c\tCXf\x088\xde\xec\x02\x00\x00H\x07\x00\x00\t\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa4\x81\x00\x00\x00\x00README.mdUT\x05\x00\x03:!\xb9eux\x0b\x00\x01\x04\xe8\x03\x00\x00\x04\xe8\x03\x00\x00PK\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00O\x00\x00\x00/\x03\x00\x00\x00\x00'
+
+
+class TestSha256VerifyerDownloader(TestCase):
+
+    @patch("os.path.exists", return_value=True)
+    def test_init(self, mock_exists):
+        s = Sha256Verifyer(filename="test.mock")
+        self.assertEqual(s.filename, "test.mock")
+        self.assertEqual(s.read_mode, "rb")
+        self.assertEqual(s.data, None)
+        mock_exists.assert_called_once_with("test.mock")
+
+    @patch("os.path.exists", return_value=False)
+    def test_fail_init(self, mock_exists):
+        with self.assertRaises(ValueError) as exc_info:
+            Sha256Verifyer(filename="test.mock")
+            mock_exists.assert_called_once_with("test.mock")
+
+        self.assertEqual(
+            str(exc_info.exception),
+            "File test.mock do not exist",
+        )
+
+    @patch("os.path.exists", return_value=True)
+    @patch("builtins.open", new_callable=mock_open, read_data=MOCK_ZIP)
+    def test_load(self, open_mock, mock_exists):
+        sha = Sha256Verifyer(filename="test.mock")
+        sha.load()
+        mock_exists.assert_called_once_with("test.mock")
+        open_mock.assert_called_once_with("test.mock", "rb")
+        self.assertEqual(sha.data, MOCK_SHA)
+
+    @patch("os.path.exists", return_value=True)
+    @patch("builtins.open", new_callable=mock_open, read_data=MOCK_ZIP)
+    def test_verify(self, open_mock, mock_exists):
+        sha = Sha256Verifyer(filename="test.mock")
+        sha.load()
+        mock_exists.assert_called_once_with("test.mock")
+        open_mock.assert_called_once_with("test.mock", "rb")
+        verify = sha.verify(MOCK_SHA)
+        self.assertTrue(verify)
diff --git a/tests/test_019_sig_verifyer.py b/tests/test_019_sig_verifyer.py
new file mode 100644
index 00000000..e1dbcf81
--- /dev/null
+++ b/tests/test_019_sig_verifyer.py
@@ -0,0 +1,92 @@
+from unittest import TestCase
+from unittest.mock import patch, mock_open
+from src.utils.verifyer.sig_verifyer import SigVerifyer
+
+MOCK_PEM = b"""-----BEGIN PUBLIC KEY-----
+MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEax27s943R8m9u/80/soZST64+JGLyckJ
+XiyFGjKuWRkpLJzCGPW40sbXZcMOSvPBvCU2vs8Hkyfyhy4lSbySfA==
+-----END PUBLIC KEY-----
+"""
+
+# pylint: disable=line-too-long
+MOCK_SIG = b"0E\x02!\x00\xed\xfb\xb2\x99\x06\x99\x97fDQ\x0f%\xdf=\xe7^h\xd1\xb6n\x16\x9cBm\xc4\xcc\xbbb:P\xb5#\x02 f\xee\xf8\x95\xfd'sqH\x9eO\xa3x\xb6>\xdc\x83\x96\xd1\xf7\x92\xcf&W\xf4n\xc0\xd3\xc8\xfe\xd3\xfd"
+
+# pylint: disable=line-too-long
+MOCK_SIG_FAIL = b"0E\x03!\x00\xed\xfb\xb2\x99\x06\x99\x97fDQ\x0f%\xdf=\xe7^h\xd1\xb6n\x16\x9cBm\xc4\xcc\xbbb:P\xb5#\x02 f\xee\xf8\x95\xfd'sqH\x9eO\xa3x\xb6>\xdc\x83\x96\xd1\xf7\x92\xcf&W\xf4n\xc0\xd3\xc8\xfe\xd3\xfd"
+
+# pylint: disable=line-too-long
+MOCK_ZIP = b'PK\x03\x04\x14\x00\x00\x00\x08\x00Aj>Xf\x088\xde\xec\x02\x00\x00H\x07\x00\x00\t\x00\x1c\x00README.mdUT\t\x00\x03:!\xb9ex\xe3\xbbeux\x0b\x00\x01\x04\xe8\x03\x00\x00\x04\xe8\x03\x00\x00\xadT\xc1\x8e\xd30\x10\xbd\xe7+\x06\xf5B\xa5\xa6\xdeE\x0b\x87\x95*\x04\x8b\x80\xd5\xb2\x12\x12\xcb\x01UHu\x9cib\xd5\x89\x83\xed\xa4\x9b\x1b\x1f\xc8G1\xb6\x13\xb5\xddv\x11\x8b8$J\xc6\x9e\xf7\xde<\xcfx\x027\xa6\xbd\x87\xeb\xda:\xae\x14\x9a$Y>[\xbem\xa5\xca\xa1\xe2\xb2\x86\xcc\xf0Z\x94\xdf\x9f\x97\xce5\xf6\x92\xb1B\xba\xb2\xcd\xe6BW\xcc\xa2Z\x8b\xd6:\x9d\xf7lC \xa9\x1cA\x18\x17N\xea\xda\xb2\xad6\x9b\xb5\xd2[\xcb2\x0f9\xef+\xc52\x9e\x178\xb7]\xf1:b/<\xcf\xf4?2LC\rB\xe7(t\xb7\xc3\x1d\x02s\xa9YQ\xb2\x1f\xca\xe4\x0f1\x9dAd\x1b\xd9\xf5\xac0\xbc)\xf7\x94:\xbd\xc1zq\xf3\xee\xe2\xfc\xe3\x8b\xb3\xdbo_\xa6O\x80%9\x87\x16\x03\x97\x95\x05\xa7!C\xe0\xf0\xe1\xeb5d\xdcbN\x11\xadB\xd8\x172K\xd6\x8a\xdb\x12x\x9dC\x8eY[\xc0\xd2\xa3\xfc\x95M\xc4\xf8\xc6\x82tP\xeb\xed\x0c\\\x89P`\x8d\x86;"\xe1M\xa3\xa4\xe0\xde<\xc0{\x14\xad\xc3\x19l\tK\xb7\x0e\\\xdf\xc8\xba \xce>!\xdc\xcasS\x0b84\x95\xac\xb9\nH\xcb(\xcb\x7f\xae\xa5\xa9\xb6\xdc \xe8\x9aT\xfbH\x8e\x9d\x14\xb8\xd3\xb8\'l>\xe8%\x9b\xbcDV\xa0s\xc4\x95\x92\'\x86t\xb1\xc1.\n\xb1I\xa0H\t0\x1d)RO\x11"\x91b:O\x92d2\x19=M\x92[\xbeA\xb0-\x89\xe9u\x0b%\xef\x10\x9a\x9e\x8a\xaa/\x93d\xb5Z\x91\xc1e\x12\x03\x90\xa6\x1d\x1aK\xf5\xfb\x85\x93\x99\xabF\xa33\xfd\n\xc63\xcc\x07\x94_?\xf7q*hds?n\x82\x98\xe4\xf7\x11\xea\x95\xd25\x06O\x0c6\xdaJ\xa7\xe3R\x10BV\x80\x08\x1b\xd24\xce\x00\xf8\xae\x83\x13G{\xd8J\xde\xc4\xa8\xfaz$\x8dZrl\xb0\xce\xb1\x16\x12\xed\xa1\xd6 j\xd4\x18s\xc9\xb7\xcf:\x8a\x1b4\x87\x06Mw\r\xdaZ\xb4\xc1\x85\x158n7t\x13\xd4\xbc\xa0\xf8Z\x87\xa7\xe2\xe1\xecf@\xc7\x15>\x12\x87\xd6\xd9\xd0\xab4\n\xd4i46p\xa7\xc1"u8\xa9\xe4\x1d\x97\x8ag\n\x03\x9c\x9d\x81i\x0f\x0e&\x8a\xa4\xa0\x17\x14E\x92\xca\t\xbc\x0f\\\xe0G\xec\xb1\xdd\x83\x9e\x98tW\xd2ni\xc1\xca\x8a\xf8\x0c\r\xd3i\x96Lq\xb1\x819\xb3F\x9c\n\x87jF\xaf&\xf0\x89\xaa\xa4\x89Z;\x1c\xaa\x8f\xa0\x0f\x85x3\x8ed\xec\xab8H\xe8\xfd\xf6c\tCXf\x088\xde\xec\x02\x00\x00H\x07\x00\x00\t\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xa4\x81\x00\x00\x00\x00README.mdUT\x05\x00\x03:!\xb9eux\x0b\x00\x01\x04\xe8\x03\x00\x00\x04\xe8\x03\x00\x00PK\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00O\x00\x00\x00/\x03\x00\x00\x00\x00'
+
+
+class TestSigVerifyerDownloader(TestCase):
+
+    @patch("os.path.exists", return_value=True)
+    @patch("builtins.open", new_callable=mock_open, read_data=MOCK_ZIP)
+    def test_init_zip(self, open_mock, mock_exists):
+        sig = SigVerifyer(
+            filename="test.zip", signature=MOCK_SIG, pubkey=MOCK_PEM, regexp=r".*\.zip"
+        )
+        sig.load()
+
+        mock_exists.assert_called_once_with("test.zip")
+        open_mock.assert_called_once_with("test.zip", "rb")
+
+    @patch("os.path.exists", return_value=True)
+    @patch("builtins.open", new_callable=mock_open, read_data=MOCK_ZIP)
+    def test_fail_init_zip(self, open_mock, mock_exists):
+        with self.assertRaises(ValueError) as exc_info:
+            SigVerifyer(
+                filename="test.zip",
+                signature=MOCK_SIG,
+                pubkey=MOCK_PEM,
+                regexp=r".*\.txt",
+            )
+            mock_exists.assert_called_once_with("test.zip")
+            open_mock.assert_called_once_with("test.zip", "rb")
+
+        self.assertEqual(
+            str(exc_info.exception),
+            "Invalid file: test.zip do not assert with .*\\.txt",
+        )
+
+    @patch("os.path.exists", return_value=True)
+    @patch("builtins.open", new_callable=mock_open, read_data=MOCK_ZIP)
+    def test_init_any(self, open_mock, mock_exists):
+        sig = SigVerifyer(
+            filename="test", signature=MOCK_SIG, pubkey=MOCK_PEM, regexp=r".*"
+        )
+        sig.load()
+
+        mock_exists.assert_called_once_with("test")
+        open_mock.assert_called_once_with("test", "rb")
+
+    @patch("os.path.exists", return_value=True)
+    @patch("builtins.open", new_callable=mock_open, read_data=MOCK_ZIP)
+    def test_verify(self, open_mock, mock_exists):
+        sig = SigVerifyer(
+            filename="test.zip", signature=MOCK_SIG, pubkey=MOCK_PEM, regexp=r".*\.zip"
+        )
+        sig.load()
+
+        result = sig.verify()
+
+        mock_exists.assert_called_once_with("test.zip")
+        open_mock.assert_called_once_with("test.zip", "rb")
+        self.assertEqual(result, True)
+
+    @patch("os.path.exists", return_value=True)
+    @patch("builtins.open", new_callable=mock_open, read_data=MOCK_ZIP)
+    def test_fail_verify_sig(self, open_mock, mock_exists):
+        sig = SigVerifyer(
+            filename="test.zip",
+            signature=MOCK_SIG_FAIL,
+            pubkey=MOCK_PEM,
+            regexp=r".*\.zip",
+        )
+        sig.load()
+
+        result = sig.verify()
+
+        mock_exists.assert_called_once_with("test.zip")
+        open_mock.assert_called_once_with("test.zip", "rb")
+        self.assertEqual(result, False)
diff --git a/tests/test_020_base_unzip.py b/tests/test_020_base_unzip.py
new file mode 100644
index 00000000..bb636e28
--- /dev/null
+++ b/tests/test_020_base_unzip.py
@@ -0,0 +1,94 @@
+import tempfile
+import zipfile
+from unittest import TestCase
+from unittest.mock import patch, call, mock_open
+from src.utils.unzip.base_unzip import BaseUnzip
+from .shared_mocks import PropertyInstanceMock, MockZipFile
+
+
+class TestBaseUnzip(TestCase):
+
+    @patch("os.path.exists", return_value=True)
+    def test_init(self, mock_exists):
+        unzip = BaseUnzip(filename="test.zip", members=["README.md"])
+        mock_exists.assert_has_calls([call("test.zip"), call(tempfile.gettempdir())])
+        self.assertEqual(unzip.filename, "test.zip")
+        self.assertIn("README.md", unzip.members)
+        self.assertEqual(unzip.output, tempfile.gettempdir())
+
+    @patch("os.path.exists", side_effect=[True, False])
+    def test_fail_init_not_exists(self, mock_exist):
+        with self.assertRaises(ValueError) as exc_info:
+            BaseUnzip(filename="test.zip", members=["README.md"])
+            mock_exist.assert_has_calls([call("test.zip"), call(tempfile.gettempdir())])
+        self.assertEqual(
+            str(exc_info.exception), f"Given path not exist: {tempfile.gettempdir()}"
+        )
+
+    @patch("src.utils.unzip.base_unzip.ZipFile")
+    @patch("os.path.exists", return_value=True)
+    def test_fail_init_empty_zip(self, mock_exists, mock_zipfile):
+
+        with self.assertRaises(ValueError) as exc_info:
+            BaseUnzip(filename="test.zip", members=[])
+            mock_exists.assert_has_calls(
+                [call("test.zip"), call(tempfile.gettempdir())]
+            )
+            mock_zipfile.assert_called_once_with("test.zip", "r")
+        self.assertEqual(str(exc_info.exception), "Members cannot be empty")
+
+    @patch(
+        "src.utils.unzip.base_unzip.BaseUnzip.members",
+        new_callable=PropertyInstanceMock,
+    )
+    @patch("os.path.exists", return_value=True)
+    def test_members(self, mock_exists, mock_members):
+        unzip = BaseUnzip(filename="test.zip", members=["README.md"])
+        mock_exists.assert_has_calls([call("test.zip"), call(tempfile.gettempdir())])
+        mock_members.assert_called_once_with(unzip, ["README.md"])
+
+    @patch(
+        "src.utils.unzip.base_unzip.BaseUnzip.members",
+        new_callable=PropertyInstanceMock,
+    )
+    @patch("os.path.exists", return_value=True)
+    def test_output(self, mock_exists, mock_members):
+        unzip = BaseUnzip(filename="test.zip", members=["README.md"])
+        mock_exists.assert_has_calls([call("test.zip"), call(tempfile.gettempdir())])
+        mock_members.assert_called_once_with(unzip, ["README.md"])
+
+    @patch("src.utils.unzip.base_unzip.ZipFile")
+    @patch("os.path.exists", return_value=True)
+    def test_load_extract(self, mock_exists, mock_zipfile):
+        mock_zipfile.return_value = MockZipFile()
+        mock_zipfile.return_value.open = mock_open
+
+        with patch.object(MockZipFile, "extract") as mock_extract:
+            unzip = BaseUnzip(filename="test.zip", members=["README.md"])
+            mock_exists.assert_has_calls(
+                [call("test.zip"), call(tempfile.gettempdir())]
+            )
+            unzip.load()
+            mock_extract.assert_called_once_with(
+                "README.md", path=tempfile.gettempdir()
+            )
+
+    @patch("src.utils.unzip.base_unzip.ZipFile")
+    @patch("os.path.exists", return_value=True)
+    def test_fail_load_badfile(self, mock_exists, mock_zipfile):
+
+        mock_zipfile.side_effect = zipfile.BadZipfile
+
+        with self.assertRaises(RuntimeError) as exc_info:
+            unzip = BaseUnzip(filename="test.zip", members=["README.md"])
+            mock_exists.assert_has_calls(
+                [call("test.zip"), call(tempfile.gettempdir())]
+            )
+
+            unzip.load()
+        self.assertEqual(str(exc_info.exception), "Cannot open test.zip: None")
+
+    def test_sanitized_base_name(self):
+        path = f"{tempfile.gettempdir()}/test/mock.zip"
+        sanitized = BaseUnzip.sanitized_base_name(path)
+        self.assertEqual(sanitized, "mock")
diff --git a/tests/test_021_kboot_unzip.py b/tests/test_021_kboot_unzip.py
new file mode 100644
index 00000000..71b254f1
--- /dev/null
+++ b/tests/test_021_kboot_unzip.py
@@ -0,0 +1,55 @@
+import tempfile
+from unittest import TestCase
+from unittest.mock import patch, call
+from src.utils.unzip.kboot_unzip import KbootUnzip
+
+
+class TestKbootUnzip(TestCase):
+
+    @patch("os.path.exists", return_value=True)
+    def test_init_m5stickv(self, mock_exists):
+        unzip = KbootUnzip(filename="test.zip", device="m5stickv")
+        mock_exists.assert_has_calls([call("test.zip"), call(tempfile.gettempdir())])
+        self.assertEqual(unzip.filename, "test.zip")
+        self.assertIn("test/maixpy_m5stickv/kboot.kfpkg", unzip.members)
+        self.assertEqual(unzip.output, tempfile.gettempdir())
+
+    @patch("os.path.exists", return_value=True)
+    def test_init_amigo_tft(self, mock_exists):
+        unzip = KbootUnzip(filename="test.zip", device="amigo_tft")
+        mock_exists.assert_has_calls([call("test.zip"), call(tempfile.gettempdir())])
+        self.assertEqual(unzip.filename, "test.zip")
+        self.assertIn("test/maixpy_amigo_tft/kboot.kfpkg", unzip.members)
+        self.assertEqual(unzip.output, tempfile.gettempdir())
+
+    @patch("os.path.exists", return_value=True)
+    def test_init_amigo_ips(self, mock_exists):
+        unzip = KbootUnzip(filename="test.zip", device="amigo_ips")
+        mock_exists.assert_has_calls([call("test.zip"), call(tempfile.gettempdir())])
+        self.assertEqual(unzip.filename, "test.zip")
+        self.assertIn("test/maixpy_amigo_ips/kboot.kfpkg", unzip.members)
+        self.assertEqual(unzip.output, tempfile.gettempdir())
+
+    @patch("os.path.exists", return_value=True)
+    def test_init_dock(self, mock_exists):
+        unzip = KbootUnzip(filename="test.zip", device="dock")
+        mock_exists.assert_has_calls([call("test.zip"), call(tempfile.gettempdir())])
+        self.assertEqual(unzip.filename, "test.zip")
+        self.assertIn("test/maixpy_dock/kboot.kfpkg", unzip.members)
+        self.assertEqual(unzip.output, tempfile.gettempdir())
+
+    @patch("os.path.exists", return_value=True)
+    def test_init_bit(self, mock_exists):
+        unzip = KbootUnzip(filename="test.zip", device="bit")
+        mock_exists.assert_has_calls([call("test.zip"), call(tempfile.gettempdir())])
+        self.assertEqual(unzip.filename, "test.zip")
+        self.assertIn("test/maixpy_bit/kboot.kfpkg", unzip.members)
+        self.assertEqual(unzip.output, tempfile.gettempdir())
+
+    @patch("os.path.exists", return_value=True)
+    def test_init_yahboom(self, mock_exists):
+        unzip = KbootUnzip(filename="test.zip", device="yahboom")
+        mock_exists.assert_has_calls([call("test.zip"), call(tempfile.gettempdir())])
+        self.assertEqual(unzip.filename, "test.zip")
+        self.assertIn("test/maixpy_yahboom/kboot.kfpkg", unzip.members)
+        self.assertEqual(unzip.output, tempfile.gettempdir())
diff --git a/tests/test_022_firmware_unzip.py b/tests/test_022_firmware_unzip.py
new file mode 100644
index 00000000..a0b0bbfc
--- /dev/null
+++ b/tests/test_022_firmware_unzip.py
@@ -0,0 +1,61 @@
+import tempfile
+from unittest import TestCase
+from unittest.mock import patch, call
+from src.utils.unzip import FirmwareUnzip
+
+
+class TestFirmwareUnzip(TestCase):
+
+    @patch("os.path.exists", return_value=True)
+    def test_init_m5stickv(self, mock_exists):
+        unzip = FirmwareUnzip(filename="test.zip", device="m5stickv")
+        mock_exists.assert_has_calls([call("test.zip"), call(tempfile.gettempdir())])
+        self.assertEqual(unzip.filename, "test.zip")
+        self.assertIn("test/maixpy_m5stickv/firmware.bin", unzip.members)
+        self.assertIn("test/maixpy_m5stickv/firmware.bin.sig", unzip.members)
+        self.assertEqual(unzip.output, tempfile.gettempdir())
+
+    @patch("os.path.exists", return_value=True)
+    def test_init_amigo_tft(self, mock_exists):
+        unzip = FirmwareUnzip(filename="test.zip", device="amigo_tft")
+        mock_exists.assert_has_calls([call("test.zip"), call(tempfile.gettempdir())])
+        self.assertEqual(unzip.filename, "test.zip")
+        self.assertIn("test/maixpy_amigo_tft/firmware.bin", unzip.members)
+        self.assertIn("test/maixpy_amigo_tft/firmware.bin.sig", unzip.members)
+        self.assertEqual(unzip.output, tempfile.gettempdir())
+
+    @patch("os.path.exists", return_value=True)
+    def test_init_amigo_ips(self, mock_exists):
+        unzip = FirmwareUnzip(filename="test.zip", device="amigo_ips")
+        mock_exists.assert_has_calls([call("test.zip"), call(tempfile.gettempdir())])
+        self.assertEqual(unzip.filename, "test.zip")
+        self.assertIn("test/maixpy_amigo_ips/firmware.bin", unzip.members)
+        self.assertIn("test/maixpy_amigo_ips/firmware.bin.sig", unzip.members)
+        self.assertEqual(unzip.output, tempfile.gettempdir())
+
+    @patch("os.path.exists", return_value=True)
+    def test_init_dock(self, mock_exists):
+        unzip = FirmwareUnzip(filename="test.zip", device="dock")
+        mock_exists.assert_has_calls([call("test.zip"), call(tempfile.gettempdir())])
+        self.assertEqual(unzip.filename, "test.zip")
+        self.assertIn("test/maixpy_dock/firmware.bin", unzip.members)
+        self.assertIn("test/maixpy_dock/firmware.bin.sig", unzip.members)
+        self.assertEqual(unzip.output, tempfile.gettempdir())
+
+    @patch("os.path.exists", return_value=True)
+    def test_init_bit(self, mock_exists):
+        unzip = FirmwareUnzip(filename="test.zip", device="bit")
+        mock_exists.assert_has_calls([call("test.zip"), call(tempfile.gettempdir())])
+        self.assertEqual(unzip.filename, "test.zip")
+        self.assertIn("test/maixpy_bit/firmware.bin", unzip.members)
+        self.assertIn("test/maixpy_bit/firmware.bin.sig", unzip.members)
+        self.assertEqual(unzip.output, tempfile.gettempdir())
+
+    @patch("os.path.exists", return_value=True)
+    def test_init_yahboom(self, mock_exists):
+        unzip = FirmwareUnzip(filename="test.zip", device="yahboom")
+        mock_exists.assert_has_calls([call("test.zip"), call(tempfile.gettempdir())])
+        self.assertEqual(unzip.filename, "test.zip")
+        self.assertIn("test/maixpy_yahboom/firmware.bin", unzip.members)
+        self.assertIn("test/maixpy_yahboom/firmware.bin.sig", unzip.members)
+        self.assertEqual(unzip.output, tempfile.gettempdir())
diff --git a/tests/test_023_base_flasher.py b/tests/test_023_base_flasher.py
new file mode 100644
index 00000000..c92083ec
--- /dev/null
+++ b/tests/test_023_base_flasher.py
@@ -0,0 +1,167 @@
+from unittest import TestCase
+from unittest.mock import MagicMock, patch
+
+from serial import SerialException
+from src.utils.flasher.base_flasher import BaseFlasher
+from .shared_mocks import MockListPortsGrep
+
+
+class TestBaseFlasher(TestCase):
+
+    @patch("os.path.exists", return_value=True)
+    def test_set_firmware(self, mock_exists):
+        b = BaseFlasher()
+        b.firmware = "mock/test/kboot.kfpkg"
+        mock_exists.assert_called_once_with("mock/test/kboot.kfpkg")
+
+    @patch("os.path.exists", return_value=False)
+    def test_fail_set_firmware(self, mock_exists):
+        with self.assertRaises(ValueError) as exc_info:
+            b = BaseFlasher()
+            b.firmware = "mock/test/kboot.kfpkg"
+            mock_exists.assert_called_once_with("mock/test/kboot.kfpkg")
+
+        self.assertEqual(
+            str(exc_info.exception), "File do not exist: mock/test/kboot.kfpkg"
+        )
+
+    @patch(
+        "src.utils.flasher.base_flasher.list_ports.grep", new_callable=MockListPortsGrep
+    )
+    def test_set_port_amigo(self, mock_grep):
+        f = BaseFlasher()
+        f.port = "amigo"
+        mock_grep.assert_called_once_with("0403")
+
+    @patch(
+        "src.utils.flasher.base_flasher.list_ports.grep", new_callable=MockListPortsGrep
+    )
+    def test_set_port_amigo_tft(self, mock_grep):
+        f = BaseFlasher()
+        f.port = "amigo_tft"
+        mock_grep.assert_called_once_with("0403")
+
+    @patch(
+        "src.utils.flasher.base_flasher.list_ports.grep", new_callable=MockListPortsGrep
+    )
+    def test_set_port_amigo_ips(self, mock_grep):
+        f = BaseFlasher()
+        f.port = "amigo_ips"
+        mock_grep.assert_called_once_with("0403")
+
+    @patch(
+        "src.utils.flasher.base_flasher.list_ports.grep", new_callable=MockListPortsGrep
+    )
+    def test_set_port_m5stickv(self, mock_grep):
+        f = BaseFlasher()
+        f.port = "m5stickv"
+        mock_grep.assert_called_once_with("0403")
+
+    @patch(
+        "src.utils.flasher.base_flasher.list_ports.grep", new_callable=MockListPortsGrep
+    )
+    def test_set_port_bit(self, mock_grep):
+        f = BaseFlasher()
+        f.port = "bit"
+        mock_grep.assert_called_once_with("0403")
+
+    @patch(
+        "src.utils.flasher.base_flasher.list_ports.grep", new_callable=MockListPortsGrep
+    )
+    def test_set_ports_cube(self, mock_grep):
+        f = BaseFlasher()
+        f.port = "cube"
+        mock_grep.assert_called_once_with("0403")
+
+    @patch(
+        "src.utils.flasher.base_flasher.list_ports.grep", new_callable=MockListPortsGrep
+    )
+    def test_set_port_dock(self, mock_grep):
+        f = BaseFlasher()
+        f.port = "dock"
+        mock_grep.assert_called_once_with("7523")
+
+    @patch(
+        "src.utils.flasher.base_flasher.list_ports.grep", new_callable=MockListPortsGrep
+    )
+    def test_set_port_yahboom(self, mock_grep):
+        f = BaseFlasher()
+        f.port = "yahboom"
+        mock_grep.assert_called_once_with("7523")
+
+    def test_fail_set_port(self):
+        with self.assertRaises(ValueError) as exc_info:
+            f = BaseFlasher()
+            f.port = "mock"
+
+        self.assertEqual(str(exc_info.exception), "Device not implemented: mock")
+
+    def test_set_board_amigo(self):
+        f = BaseFlasher()
+        f.board = "amigo"
+        self.assertEqual(f.board, "goE")
+
+    def test_set_board_amigo_tft(self):
+        f = BaseFlasher()
+        f.board = "amigo_tft"
+        self.assertEqual(f.board, "goE")
+
+    def test_set_board_amigo_ips(self):
+        f = BaseFlasher()
+        f.board = "amigo_ips"
+        self.assertEqual(f.board, "goE")
+
+    def test_set_board_m5stickv(self):
+        f = BaseFlasher()
+        f.board = "m5stickv"
+        self.assertEqual(f.board, "goE")
+
+    def test_set_board_bit(self):
+        f = BaseFlasher()
+        f.board = "bit"
+        self.assertEqual(f.board, "goE")
+
+    def test_set_board_dock(self):
+        f = BaseFlasher()
+        f.board = "dock"
+        self.assertEqual(f.board, "dan")
+
+    def test_set_board_yahboom(self):
+        f = BaseFlasher()
+        f.board = "yahboom"
+        self.assertEqual(f.board, "goE")
+
+    def test_set_board_cube(self):
+        f = BaseFlasher()
+        f.board = "cube"
+        self.assertEqual(f.board, "goE")
+
+    def test_fail_set_board(self):
+        with self.assertRaises(ValueError) as exc_info:
+            f = BaseFlasher()
+            f.board = "mock"
+        self.assertEqual(str(exc_info.exception), "Device not implemented: mock")
+
+    def test_set_print_callback(self):
+        f = BaseFlasher()
+        f.print_callback = MagicMock()
+        f.print_callback()
+        f.print_callback.assert_called_once()
+
+    @patch("src.utils.flasher.base_flasher.Serial", side_effect=SerialException())
+    def test_fail_is_port_working(self, mock_serial):
+
+        f = BaseFlasher()
+        result = f.is_port_working(port="mock")
+        self.assertFalse(result)
+
+        mock_serial.assert_called_once_with("mock")
+
+    @patch("src.utils.flasher.base_flasher.Serial")
+    def test_is_port_working(self, mock_serial):
+
+        f = BaseFlasher()
+        result = f.is_port_working(port="mock")
+        self.assertTrue(result)
+
+        mock_serial.assert_called_once_with("mock")
diff --git a/tests/test_025_flasher.py b/tests/test_025_flasher.py
new file mode 100644
index 00000000..7334ef02
--- /dev/null
+++ b/tests/test_025_flasher.py
@@ -0,0 +1,262 @@
+from unittest import TestCase
+from unittest.mock import patch, MagicMock, call
+from src.utils.flasher import Flasher
+from .shared_mocks import MockListPortsGrep
+
+
+class TestFlasher(TestCase):
+
+    @patch("os.path.exists", return_value=True)
+    @patch("src.utils.flasher.base_flasher.list_ports", new_callable=MockListPortsGrep)
+    @patch("src.utils.flasher.base_flasher.next")
+    @patch("src.utils.flasher.flasher.Flasher.is_port_working", return_value=True)
+    @patch("src.utils.kboot.build.ktool.KTool.process")
+    def test_flash_success(
+        self,
+        mock_process,
+        mock_is_port_working,
+        mock_next,
+        mock_list_ports,
+        mock_exists,
+    ):
+        mock_next.return_value = MagicMock(device="mock")
+        callback = MagicMock()
+        f = Flasher()
+        f.firmware = "mock/maixpy_amigo/kboot.kfpkg"
+        f.baudrate = 1500000
+        f.flash(callback=callback)
+        mock_exists.assert_called_once_with("mock/maixpy_amigo/kboot.kfpkg")
+        mock_list_ports.grep.assert_called_once_with("0403")
+        mock_next.assert_called_once()
+        mock_is_port_working.assert_has_calls(
+            [
+                call(mock_next().device),
+            ],
+            any_order=True,
+        )
+        mock_process.assert_called_once_with(
+            terminal=False,
+            dev="mock",
+            baudrate=1500000,
+            board="goE",
+            file="mock/maixpy_amigo/kboot.kfpkg",
+            callback=callback,
+        )
+
+    @patch("os.path.exists", return_value=False)
+    def test_fail_flash_firmware_not_exist(self, mock_exists):
+        with self.assertRaises(ValueError) as exc_info:
+            f = Flasher()
+            f.firmware = "mock/maixpy_amigo/kboot.kfpkg"
+
+        self.assertEqual(
+            str(exc_info.exception), "File do not exist: mock/maixpy_amigo/kboot.kfpkg"
+        )
+        mock_exists.assert_called_once_with("mock/maixpy_amigo/kboot.kfpkg")
+
+    @patch("os.path.exists", return_value=True)
+    def test_fail_flash_wrong_baudrate(self, mock_exists):
+        with self.assertRaises(ValueError) as exc_info:
+            f = Flasher()
+            f.firmware = "mock/maixpy_amigo/kboot.kfpkg"
+            f.baudrate = 1234567
+
+        self.assertEqual(str(exc_info.exception), "Invalid baudrate: 1234567")
+        mock_exists.assert_called_once_with("mock/maixpy_amigo/kboot.kfpkg")
+
+    @patch("os.path.exists", return_value=True)
+    @patch("src.utils.flasher.base_flasher.list_ports", new_callable=MockListPortsGrep)
+    @patch("src.utils.flasher.base_flasher.next")
+    @patch("src.utils.flasher.flasher.Flasher.is_port_working", return_value=True)
+    @patch("src.utils.kboot.build.ktool.KTool.process")
+    def test_flash_after_first_greeting_fail(
+        self,
+        mock_process,
+        mock_is_port_working,
+        mock_next,
+        mock_list_ports,
+        mock_exists,
+    ):
+        mock_exception = Exception("Greeting fail: mock test")
+        mock_process.side_effect = [mock_exception, True]
+        mock_next.side_effect = [MagicMock(device="mocked")]
+        mock_list_ports.grep.return_value.__next__.side_effect = [
+            MagicMock(device="mocked_next")
+        ]
+
+        callback = MagicMock()
+        f = Flasher()
+        f.firmware = "mock/maixpy_amigo/kboot.kfpkg"
+        f.baudrate = 1500000
+        f.flash(callback=callback)
+
+        # patch assertions
+        mock_exists.assert_called_once_with("mock/maixpy_amigo/kboot.kfpkg")
+        mock_list_ports.grep.assert_called_once_with("0403")
+        mock_next.assert_called_once()
+        mock_is_port_working.assert_has_calls(
+            [
+                call("mocked"),
+                call("mocked_next"),
+            ]
+        )
+        mock_process.assert_has_calls(
+            [
+                call(
+                    terminal=False,
+                    dev="mocked",
+                    baudrate=1500000,
+                    board="goE",
+                    file="mock/maixpy_amigo/kboot.kfpkg",
+                    callback=callback,
+                ),
+                call(
+                    terminal=False,
+                    dev="mocked_next",
+                    baudrate=1500000,
+                    board="goE",
+                    file="mock/maixpy_amigo/kboot.kfpkg",
+                    callback=callback,
+                ),
+            ]
+        )
+
+    @patch("os.path.exists", return_value=True)
+    @patch("src.utils.flasher.base_flasher.list_ports", new_callable=MockListPortsGrep)
+    @patch("src.utils.flasher.base_flasher.next")
+    @patch("src.utils.flasher.flasher.Flasher.is_port_working", return_value=False)
+    @patch("src.utils.flasher.base_flasher.KTool.log")
+    def test_fail_flash_port_not_working(
+        self,
+        mock_ktool_log,
+        mock_is_port_working,
+        mock_next,
+        mock_list_ports,
+        mock_exists,
+    ):
+        mock_next.return_value = MagicMock(device="mock")
+        callback = MagicMock()
+
+        f = Flasher()
+        f.firmware = "mock/maixpy_amigo/kboot.kfpkg"
+        f.baudrate = 1500000
+        f.flash(callback=callback)
+        mock_exists.assert_called_once_with("mock/maixpy_amigo/kboot.kfpkg")
+        mock_list_ports.grep.assert_called_once_with("0403")
+        mock_next.assert_called_once()
+        mock_is_port_working.assert_has_calls(
+            [
+                call(mock_next().device),
+            ],
+            any_order=True,
+        )
+        mock_ktool_log.assert_called_once_with("Port mock not working")
+
+    @patch("os.path.exists", return_value=True)
+    @patch("src.utils.flasher.base_flasher.list_ports", new_callable=MockListPortsGrep)
+    @patch("src.utils.flasher.base_flasher.next")
+    @patch(
+        "src.utils.flasher.flasher.Flasher.is_port_working", side_effect=[True, False]
+    )
+    @patch("src.utils.kboot.build.ktool.KTool.process")
+    @patch("src.utils.flasher.base_flasher.KTool.log")
+    def test_fail_flash_after_first_greeting_fail_port_not_working(
+        self,
+        mock_ktool_log,
+        mock_process,
+        mock_is_port_working,
+        mock_next,
+        mock_list_ports,
+        mock_exists,
+    ):
+        mock_exception = RuntimeError("Greeting fail: mock test")
+        mock_process.side_effect = [mock_exception]
+        mock_next.side_effect = [MagicMock(device="mocked")]
+        mock_list_ports.grep.return_value.__next__.side_effect = [
+            MagicMock(device="mocked_next")
+        ]
+
+        callback = MagicMock()
+        f = Flasher()
+        f.firmware = "mock/maixpy_amigo/kboot.kfpkg"
+        f.baudrate = 1500000
+        f.flash(callback=callback)
+
+        # patch assertions
+        mock_exists.assert_called_once_with("mock/maixpy_amigo/kboot.kfpkg")
+        mock_list_ports.grep.assert_called_once_with("0403")
+        mock_next.assert_called_once()
+        mock_is_port_working.assert_has_calls([call("mocked"), call("mocked_next")])
+        mock_process.assert_has_calls(
+            [
+                call(
+                    terminal=False,
+                    dev="mocked",
+                    baudrate=1500000,
+                    board="goE",
+                    file="mock/maixpy_amigo/kboot.kfpkg",
+                    callback=callback,
+                ),
+            ]
+        )
+        mock_ktool_log.assert_has_calls(
+            [
+                call("Greeting fail: mock test for mocked"),
+                call(""),
+                call("Port mocked_next not working"),
+            ]
+        )
+
+    @patch("os.path.exists", return_value=True)
+    @patch("src.utils.flasher.base_flasher.list_ports", new_callable=MockListPortsGrep)
+    @patch("src.utils.flasher.base_flasher.next")
+    @patch(
+        "src.utils.flasher.flasher.Flasher.is_port_working", side_effect=[True, True]
+    )
+    @patch("src.utils.kboot.build.ktool.KTool.process")
+    @patch("src.utils.flasher.base_flasher.KTool.log")
+    def test_fail_flash_after_first_greeting_fail_stop_iteration(
+        self,
+        mock_ktool_log,
+        mock_process,
+        mock_is_port_working,
+        mock_next,
+        mock_list_ports,
+        mock_exists,
+    ):
+        mock_exception = Exception("Greeting fail: mock test")
+        mock_process.side_effect = [mock_exception, True]
+
+        mock_next.side_effect = [MagicMock(device="mocked")]
+        mock_list_ports.grep.return_value.__next__.side_effect = [StopIteration()]
+
+        callback = MagicMock()
+        f = Flasher()
+        f.firmware = "mock/maixpy_amigo/kboot.kfpkg"
+        f.baudrate = 1500000
+        f.flash(callback=callback)
+
+        # patch assertions
+        mock_exists.assert_called_once_with("mock/maixpy_amigo/kboot.kfpkg")
+        mock_list_ports.grep.assert_called_once_with("0403")
+        mock_next.assert_called_once()
+        mock_is_port_working.assert_has_calls(
+            [
+                call("mocked"),
+            ]
+        )
+        mock_process.assert_has_calls(
+            [
+                call(
+                    terminal=False,
+                    dev="mocked",
+                    baudrate=1500000,
+                    board="goE",
+                    file="mock/maixpy_amigo/kboot.kfpkg",
+                    callback=callback,
+                ),
+            ]
+        )
+        mock_ktool_log.assert_has_calls(
+            [call("Greeting fail: mock test for mocked"), call("")]
+        )
diff --git a/tests/test_026_wiper.py b/tests/test_026_wiper.py
new file mode 100644
index 00000000..992e8578
--- /dev/null
+++ b/tests/test_026_wiper.py
@@ -0,0 +1,185 @@
+# The MIT License (MIT)
+
+# Copyright (c) 2021-2024 Krux contributors
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+"""
+base_flasher.py
+"""
+from unittest import TestCase
+from unittest.mock import patch, call, MagicMock
+from src.utils.flasher import Wiper
+from .shared_mocks import MockListPortsGrep
+
+
+class TestWiper(TestCase):
+
+    @patch("src.utils.flasher.base_flasher.list_ports", new_callable=MockListPortsGrep)
+    @patch("src.utils.flasher.base_flasher.next")
+    @patch("src.utils.flasher.wiper.Wiper.is_port_working", return_value=True)
+    @patch("src.utils.kboot.build.ktool.KTool.process")
+    def test_wipe_success(
+        self, mock_process, mock_is_port_working, mock_next, mock_list_ports
+    ):
+        f = Wiper()
+        f.baudrate = 1500000
+        f.wipe(device="amigo")
+        mock_list_ports.grep.assert_called_once_with("0403")
+        mock_next.assert_called_once()
+        mock_is_port_working.assert_called_once_with(mock_next().device)
+        mock_process.assert_called_once()
+
+    def test_fail_wipe_wrong_baudrate(self):
+        with self.assertRaises(ValueError) as exc_info:
+            f = Wiper()
+            f.baudrate = 1234567
+
+        self.assertEqual(str(exc_info.exception), "Invalid baudrate: 1234567")
+
+    @patch("src.utils.flasher.base_flasher.list_ports", new_callable=MockListPortsGrep)
+    @patch("src.utils.flasher.base_flasher.next")
+    @patch("src.utils.flasher.wiper.Wiper.is_port_working", return_value=True)
+    @patch("src.utils.kboot.build.ktool.KTool.process")
+    def test_wipe_after_first_greeting_fail(
+        self,
+        mock_process,
+        mock_is_port_working,
+        mock_next,
+        mock_list_ports,
+    ):
+        mock_exception = Exception("Greeting fail: mock test")
+        mock_process.side_effect = [mock_exception, True]
+        mock_next.side_effect = [MagicMock(device="mocked")]
+        mock_list_ports.grep.return_value.__next__.side_effect = [
+            MagicMock(device="mocked_next")
+        ]
+
+        f = Wiper()
+        f.baudrate = 1500000
+        f.wipe(device="amigo")
+
+        # patch assertions
+        mock_list_ports.grep.assert_called_once_with("0403")
+        mock_next.assert_called_once()
+        mock_is_port_working.assert_has_calls(
+            [
+                call("mocked"),
+                call("mocked_next"),
+            ]
+        )
+        mock_process.assert_has_calls([call(), call()])
+
+    @patch("src.utils.flasher.base_flasher.list_ports", new_callable=MockListPortsGrep)
+    @patch("src.utils.flasher.base_flasher.next")
+    @patch("src.utils.flasher.wiper.Wiper.is_port_working", return_value=False)
+    @patch("src.utils.flasher.base_flasher.KTool.log")
+    def test_fail_wipe_port_not_working(
+        self, mock_ktool_log, mock_is_port_working, mock_next, mock_list_ports
+    ):
+        mock_next.return_value = MagicMock(device="mocked")
+
+        f = Wiper()
+        f.baudrate = 1500000
+        f.wipe(device="amigo")
+        mock_list_ports.grep.assert_called_once_with("0403")
+        mock_next.assert_called_once()
+        mock_is_port_working.assert_has_calls(
+            [
+                call("mocked"),
+            ],
+            any_order=True,
+        )
+        mock_ktool_log.assert_called_once_with("Port mocked not working")
+
+    @patch("src.utils.flasher.base_flasher.list_ports", new_callable=MockListPortsGrep)
+    @patch("src.utils.flasher.base_flasher.next")
+    @patch("src.utils.flasher.wiper.Wiper.is_port_working", side_effect=[True, False])
+    @patch("src.utils.kboot.build.ktool.KTool.process")
+    @patch("src.utils.flasher.base_flasher.KTool.log")
+    def test_fail_wipe_after_first_greeting_fail_port_not_working(
+        self,
+        mock_ktool_log,
+        mock_process,
+        mock_is_port_working,
+        mock_next,
+        mock_list_ports,
+    ):
+        mock_exception = Exception("Greeting fail: mock test")
+        mock_process.side_effect = [mock_exception]
+        mock_next.side_effect = [MagicMock(device="mocked")]
+        mock_list_ports.grep.return_value.__next__.side_effect = [
+            MagicMock(device="mocked_next"),
+        ]
+
+        f = Wiper()
+        f.baudrate = 1500000
+        f.wipe(device="amigo")
+
+        # patch assertions
+        mock_list_ports.grep.assert_called_once_with("0403")
+        mock_next.assert_called_once()
+        mock_is_port_working.assert_has_calls(
+            [
+                call("mocked"),
+                call("mocked_next"),
+            ]
+        )
+        mock_process.assert_called_once()
+        mock_ktool_log.assert_has_calls(
+            [
+                call("Greeting fail: mock test for mocked"),
+                call(""),
+                call("Port mocked_next not working"),
+            ]
+        )
+
+    @patch("src.utils.flasher.base_flasher.list_ports", new_callable=MockListPortsGrep)
+    @patch("src.utils.flasher.base_flasher.next")
+    @patch("src.utils.flasher.wiper.Wiper.is_port_working", side_effect=[True, True])
+    @patch("src.utils.kboot.build.ktool.KTool.process")
+    @patch("src.utils.flasher.base_flasher.KTool.log")
+    def test_fail_wipe_after_first_greeting_fail_stop_iteration(
+        self,
+        mock_ktool_log,
+        mock_process,
+        mock_is_port_working,
+        mock_next,
+        mock_list_ports,
+    ):
+        mock_exception = Exception("Greeting fail: mock test")
+        mock_process.side_effect = [mock_exception, True]
+        mock_next.side_effect = [MagicMock(device="mocked")]
+        mock_list_ports.grep.return_value.__next__.side_effect = [StopIteration()]
+
+        f = Wiper()
+        f.baudrate = 1500000
+        f.wipe(device="amigo")
+
+        # patch assertions
+        mock_list_ports.grep.assert_called_once_with("0403")
+        mock_next.assert_called_once()
+        mock_is_port_working.assert_has_calls(
+            [
+                call("mocked"),
+            ]
+        )
+        # mock_process.assert_called_once()
+        mock_ktool_log.assert_has_calls(
+            [call("Greeting fail: mock test for mocked"), call("")]
+        )
diff --git a/tests/test_027_base_signer.py b/tests/test_027_base_signer.py
new file mode 100644
index 00000000..6b8957e0
--- /dev/null
+++ b/tests/test_027_base_signer.py
@@ -0,0 +1,94 @@
+from unittest import TestCase
+from unittest.mock import patch
+from src.utils.signer.base_signer import BaseSigner
+from .shared_mocks import MOCKED_SIGNATURE
+
+
+class TestBaseSigner(TestCase):
+
+    @patch("os.path.exists", return_value=True)
+    def test_filename(self, mock_exists):
+        s = BaseSigner(filename="mock.txt")
+        mock_exists.assert_called_once_with("mock.txt")
+        self.assertEqual(s.filename, "mock.txt")
+
+    @patch("os.path.exists", return_value=False)
+    def test_fail_filename(self, mock_exists):
+        with self.assertRaises(ValueError) as exc_info:
+            BaseSigner(filename="mock.txt")
+            mock_exists.assert_called_once_with("mock.txt")
+
+        self.assertEqual(str(exc_info.exception), "mock.txt do not exists")
+
+    @patch("os.path.exists", return_value=True)
+    def test_filehash(self, mock_exists):
+        s = BaseSigner(filename="mock.txt")
+        s.filehash = "5f98101992d1b411c05050dec665c16b1ddfd88aec9dd3ed55eefa046a3f4ab9"
+        mock_exists.assert_called_once_with("mock.txt")
+        self.assertEqual(
+            s.filehash,
+            "5f98101992d1b411c05050dec665c16b1ddfd88aec9dd3ed55eefa046a3f4ab9",
+        )
+
+    @patch("os.path.exists", return_value=True)
+    def test_fail_filehash(self, mock_exists):
+        with self.assertRaises(ValueError) as exc_info:
+            s = BaseSigner(filename="mock.txt")
+            s.filehash = (
+                "5h98101992i1b411j05050klm665n16o1pqfd88rst9uv3wd55eefa046a3f4ab9"
+            )
+            mock_exists.assert_called_once_with("mock.txt")
+
+        self.assertEqual(
+            str(exc_info.exception),
+            "Invalid hash: 5h98101992i1b411j05050klm665n16o1pqfd88rst9uv3wd55eefa046a3f4ab9",
+        )
+
+    @patch("os.path.exists", return_value=True)
+    def test_signature(self, mock_exists):
+        s = BaseSigner(filename="mock.txt")
+        s.signature = MOCKED_SIGNATURE
+        mock_exists.assert_called_once_with("mock.txt")
+        self.assertEqual(
+            s.signature,
+            b"".join(
+                [
+                    b"0D\x02 -\x95\x8e$T\xbb\xf52\x8c9_@",
+                    b"\x90\xab\x03\xc62
-/// 
-
-/**
- * The KruxInstaller namespace:
- * 
- * @see DebugName
- * @see JsonValue
- * @see JsonArray
- */
-declare namespace KruxInstaller {
-
-    /**
-     * The specific format `string:string` to debug in terminal or console
-     */
-    export type DebugName = `${string}:${string}`
-  
-    // 
-    /**
-     * Arbitrary value for json objects
-     * see https://stackoverflow.com/questions/64921660/dealing-with-arbitrary-objects-in-typescript
-     * 
-     * @see JsonArray
-     */
-    export type JsonValue = null | string | number | boolean | JsonArray | JsonDict;
-  
-    /**
-     * Arbitrary array of values for json objects
-     * see https://stackoverflow.com/questions/64921660/dealing-with-arbitrary-objects-in-typescript
-     * 
-     * @see JsonValue
-     */
-    export type JsonArray = JsonValue[];
-    
-    /**
-     * An arbitrary json in form {string: JsonValue}
-     * 
-     * @see JsonValue
-     */
-    export interface JsonDict extends Record { }
-
-    export interface StartedApp {
-      app: Electron.App;
-      ipcMain: Electron.IpcMain;
-      win: Electron.BrowserWindow;
-    }
-
-    export interface FetchedGithubTags {
-      ref: string;
-      node_id: string;
-      url: string;
-      object: {
-        sha: string;
-        type: string;
-        url: string;
-      }
-    }
-}
\ No newline at end of file
diff --git a/vite.config.ts b/vite.config.ts
deleted file mode 100644
index 2960fb47..00000000
--- a/vite.config.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import { rmSync } from 'node:fs'
-import { defineConfig } from 'vite'
-import vue from '@vitejs/plugin-vue'
-import vuetify from 'vite-plugin-vuetify'
-import electron from 'vite-plugin-electron'
-import { createHtmlPlugin } from 'vite-plugin-html'
-
-
-//import renderer from 'vite-plugin-electron-renderer'
-import pkg from './package.json'
-
-// https://vitejs.dev/config/
-export default defineConfig(({ command }) => {
-  rmSync('dist-electron', { recursive: true, force: true })
-
-  const isServe = command === 'serve'
-  const isBuild = command === 'build'
-  const sourcemap = isServe || !!process.env.VSCODE_DEBUG
-
-  return {
-    open: true,
-    plugins: [
-      vue(),
-      vuetify({ autoImport: true }),
-      electron([
-        {
-          entry: 'electron/main/index.ts',
-          onstart(options) {
-            
-            /* See `.vscode/.debug.script.mjs` */
-            if (process.env.VSCODE_DEBUG) {
-              console.log(`[ startup ] KruxInstaller v${pkg.version}`)
-            } else {
-              options.startup()
-            }
-          },
-          vite: {
-            build: {
-              sourcemap,
-              minify: isBuild,
-              outDir: 'dist-electron/main',
-              rollupOptions: {
-                external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}),
-              },
-            },
-          }
-        },
-        {
-          entry: 'electron/preload/index.ts',
-          onstart(options) {
-            // Notify the Renderer-Process to reload the page when the Preload-Scripts build is complete, 
-            // instead of restarting the entire Electron App.
-            if (process.env.VSCODE_DEBUG) {
-              console.log(`[ reload ] KruxInstaller v${pkg.version}`)
-            } else {
-              options.reload()
-            }
-          },
-          vite: {
-            build: {
-              sourcemap: sourcemap ? 'inline' : undefined, // #332
-              minify: isBuild,
-              outDir: 'dist-electron/preload',
-              rollupOptions: {
-                external: Object.keys('dependencies' in pkg ? pkg.dependencies : {}),
-              },
-            },
-          }
-        }
-      ]),
-      createHtmlPlugin({
-        minify: true,
-        inject: {
-          data: {
-            title: `${pkg.name} ${pkg.version}`
-          }
-        }
-      })
-    ],
-    server: process.env.VSCODE_DEBUG && (() => {
-      const url = new URL(pkg.vscode.debug.env.VITE_DEV_SERVER_URL)
-      return {
-        host: url.hostname,
-        port: +url.port,
-      }
-    })(),
-    clearScreen: false,
-  }
-})
diff --git a/wdio.conf.mts b/wdio.conf.mts
deleted file mode 100644
index 4a5b25b3..00000000
--- a/wdio.conf.mts
+++ /dev/null
@@ -1,531 +0,0 @@
-import { join, dirname } from 'path'
-import { fileURLToPath } from 'url';
-import { glob, globSync } from 'glob'
-import { rimraf } from 'rimraf'
-import { tmpdir, homedir } from 'os'
-import { createRequire } from 'module'
-import { osLangSync } from 'os-lang'
-import createDebug from 'debug'
-import { accessSync, readFileSync } from 'fs';
-import { exec, execFile } from 'child_process';
-
-const { devDependencies, version } = createRequire(import.meta.url)('./package.json')
-const debug = createDebug('krux:wdio:e2e')
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = dirname(__filename);
-
-// select the correct binary
-// for each OS
-let APP_PATH: string
-if (process.platform === 'linux') {
-  if (process.arch === 'arm64') {
-    APP_PATH = join(__dirname, 'release', version, 'linux-arm64-unpacked', 'krux-installer')
-  } else {
-    APP_PATH = join(__dirname, 'release', version, 'linux-unpacked', 'krux-installer')
-  }
-} else if (process.platform === 'win32') {
-  APP_PATH = join(__dirname, 'release', version, 'win-unpacked', 'krux-installer.exe')
-} else if (process.platform === 'darwin') {
-  APP_PATH = join(__dirname, 'release', version, 'mac', 'krux-installer.app', 'Contents', 'MacOS', 'krux-installer')
-} else {
-  throw new Error(`Platform '${process.platform}' not implemented`)
-}
-
-const APP_ARGS = [
-  '--disable-infobars',
-  '--disable-dev-shm-usage',
-  '--no-sandbox',
-  '--remote-debugging-port=9222'
-]
-
-// Define which specs to
-// test or to exclude
-let SPECS_TO_TEST: string[] = [];
-let SPECS_TO_EXCLUDE: string[] = [];
-
-// Prepare resources path
-// for specific test environment
-let resources = ''
-  
-if (process.env.CI && process.env.GITHUB_ACTION) {
-  if (process.platform  === 'linux') {
-    resources = '/home/runner/krux-installer'
-  } else if (process.platform  === 'win32') {
-    resources = 'C:\\Users\\runneradmin\\Documents\\krux-installer'
-  } else if (process.platform  === 'darwin') {
-    resources = '/Users/runner/Documents/krux-installer'
-  }
-} else {
-  const lang = osLangSync()
-  const home = homedir()
-  if ( lang.match(/en-*/g)) {
-    resources = join(home, 'Documents', 'krux-installer')
-  } else if ( lang.match(/pt-*/g)) {
-    resources = join(home, 'Documentos', 'krux-installer')
-  } else if ( lang.match(/POSIX/) ) {
-    // Check if is running under docker container (containerized build for arm64)
-    if (process.env.NODE_DOCKER) {
-      resources = join(process.env.DOCUMENTS, 'krux-installer')
-    } else {
-       throw new Error('Failed to check if is running under docker')
-    }
-  } else {
-    throw new Error(`'${lang}' lang not implemented`)
-  }
-}
-
-// Define where specs are located
-const specs = globSync('./test/e2e/specs/*.spec.mts')
-
-// If you want to filter some test, you could use
-// a `--filter` with regular expression
-const hasFilter = process.argv.slice(4)
-
-if (hasFilter.length > 0) {
-  
-  if (hasFilter[0] === '--filter' || hasFilter[0] === '-f') {
-    const filter = new RegExp(hasFilter[1], 'g')
-    specs.map(async function (file: string) {
-      if (file.match(filter)) {
-        debug(`Excluding ${file}`)
-        SPECS_TO_EXCLUDE.push(file)
-      }
-    })
-  }
-}
-
-// loop through all specs and verify
-// if some of them could have a resource test.
-// - Positive: add it to SPECS_TO_EXCLUDE
-// - Negative: add it to SPECS_TO_TEST
-specs.map(async function (file: string) {
-  if (
-    file === 'test/e2e/specs/014.select-version-selfcustody-release-zip.spec.ts' ||
-    file === 'test/e2e/specs/018.already-downloaded-selfcustody-release-zip-click-download-again.spec.ts' 
-  ) {
-    try {
-      const r = join(resources, 'v22.08.2', 'krux-v22.08.2.zip')
-      accessSync(r)
-      SPECS_TO_EXCLUDE.push(file)
-    } catch (error) {
-      SPECS_TO_TEST.push(file)
-    }
-  } else if (
-    file === 'test/e2e/specs/020.select-version-selfcustody-release-zip-sha256.spec.ts' ||
-    file === 'test/e2e/specs/024.already-downloaded-selfcustody-release-zip-sha256-click-download-again-button.spec.ts'
-  ) {
-    try {
-      const r = join(resources, 'v22.08.2', 'krux-v22.08.2.zip.sha256.txt')
-      accessSync(r)
-      SPECS_TO_EXCLUDE.push(file)
-    } catch (error) {
-      SPECS_TO_TEST.push(file)
-    }
-  } else if (
-    file === 'test/e2e/specs/026.select-version-selfcustody-release-zip-sig.spec.ts' ||
-    file === 'test/e2e/specs/030.already-downloaded-selfcustody-release-zip-sig-download-again-button.spec.ts'
-  ) {
-    try {
-      const r = join(resources, 'v22.08.2', 'krux-v22.08.2.zip.sig')
-      accessSync(r)
-      SPECS_TO_EXCLUDE.push(file)
-    } catch (error) {
-      SPECS_TO_TEST.push(file)
-    }
-  } else if (
-    file === 'test/e2e/specs/032-select-version-selfcustody-pem.spec.ts'
-  ) {
-    try {
-      const r = join(resources, 'main', 'selfcustody.pem')
-      accessSync(r)
-      SPECS_TO_EXCLUDE.push(file)
-    } catch (error) {
-      SPECS_TO_TEST.push(file)
-    }
-  }
-  else {
-    SPECS_TO_TEST.push(file)
-  }
-})
-
-export const config = {
-    //
-    // ====================
-    // Runner Configuration
-    // ====================
-    // WebdriverIO supports running e2e tests as well as unit and component tests.
-    runner: 'local',
-    //
-    // ==================
-    // Specify Test Files
-    // ==================
-    // Define which test specs should run. The pattern is relative to the directory
-    // from which `wdio` was called.
-    //
-    // The specs are defined as an array of spec files (optionally using wildcards
-    // that will be expanded). The test for each spec file will be run in a separate
-    // worker process. In order to have a group of spec files run in the same worker
-    // process simply enclose them in an array within the specs array.
-    //
-    // If you are calling `wdio` from an NPM script (see https://docs.npmjs.com/cli/run-script),
-    // then the current working directory is where your `package.json` resides, so `wdio`
-    // will be called from there.
-    //
-    specs: SPECS_TO_TEST.reverse(),
-    //featureFlags: {
-    //  specFiltering: true  
-    //},
-    // Patterns to exclude.
-    exclude: SPECS_TO_EXCLUDE.reverse(),
-    // WebdriverIO will automatically detect if these dependencies are installed
-    // and will compile your config and tests for you. Ensure to have a tsconfig.json
-    // in the same directory as you WDIO config. If you need to configure how ts-node
-    // runs please use the environment variables for ts-node or use wdio config's
-    // autoCompileOpts section.
-    autoCompileOpts: {
-      autoCompile: true,
-      // see https://github.com/TypeStrong/ts-node#cli-and-programmatic-options
-      // for all available options
-      tsNodeOpts: {
-        project: './tsconfig.e2e.json',
-        transpileOnly: true,
-      }
-    },
-    //
-    // ============
-    // Capabilities
-    // ============
-    // Define your capabilities here. WebdriverIO can run multiple capabilities at the same
-    // time. Depending on the number of capabilities, WebdriverIO launches several test
-    // sessions. Within your capabilities you can overwrite the spec and exclude options in
-    // order to group specific specs to a specific capability.
-    //
-    // First, you can define how many instances should be started at the same time. Let's
-    // say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have
-    // set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec
-    // files and you set maxInstances to 10, all spec files will get tested at the same time
-    // and 30 processes will get spawned. The property handles how many capabilities
-    // from the same test should run tests.
-    //
-    maxInstances: 1,
-    //
-    // If you have trouble getting all constant capabilities together, check out the
-    // Sauce Labs platform configurator - a great tool to configure your capabilities:
-    // https://saucelabs.com/platform/platform-configurator
-    //
-    capabilities: [{
-        // maxInstances can get overwritten per capability. So if you have an in-house Selenium
-        // grid with only 5 firefox instances available you can make sure that not more than
-        // 5 instances get started at a time.
-        maxInstances: 1,
-        browserName: 'electron',
-        'wdio:electronServiceOptions': {
-          appBinaryPath: APP_PATH,
-          appArgs: APP_ARGS,
-        }
-    }],
-    //
-    // ===================
-    // Test Configurations
-    // ===================
-    // Define all options that are relevant for the WebdriverIO instance here
-    //
-    // Level of logging verbosity: trace | debug | info | warn | error | silent
-    logLevel: 'trace',
-    //
-    // Set specific log levels per logger
-    // loggers:
-    // - webdriver, webdriverio
-    // - @wdio/browserstack-service, @wdio/devtools-service, @wdio/sauce-service
-    // - @wdio/mocha-framework, @wdio/jasmine-framework
-    // - @wdio/local-runner
-    // - @wdio/sumologic-reporter
-    // - @wdio/cli, @wdio/config, @wdio/utils
-    // Level of logging verbosity: trace | debug | info | warn | error | silent
-    // logLevels: {
-    //     webdriver: 'info',
-    //     '@wdio/appium-service': 'info'
-    // },
-    //
-    // If you only want to run your tests until a specific amount of tests have failed use
-    // bail (default is 0 - don't bail, run all tests).
-    bail: 1,
-    //
-    // Set a base URL in order to shorten url command calls. If your `url` parameter starts
-    // with `/`, the base url gets prepended, not including the path portion of your baseUrl.
-    // If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url
-    // gets prepended directly.
-    baseUrl: 'http://localhost',
-    //
-    // Default timeout for all waitFor* commands.
-    waitforTimeout: 10000,
-    //
-    // Default timeout in milliseconds for request
-    // if browser driver or grid doesn't send response
-    connectionRetryTimeout: 120000,
-    //
-    // Default request retries count
-    connectionRetryCount: 3,
-    //
-    // Test runner services
-    // Services take over a specific job you don't want to take care of. They enhance
-    // your test setup with almost no effort. Unlike plugins, they don't add new
-    // commands. Instead, they hook themselves up into the test process.
-    services: ['electron'],
-    // Framework you want to run your specs with.
-    // The following are supported: Mocha, Jasmine, and Cucumber
-    // see also: https://webdriver.io/docs/frameworks
-    //
-    // Make sure you have the wdio adapter package for the specific framework installed
-    // before running any tests.
-    framework: 'mocha',
-    //
-    // The number of times to retry the entire specfile when it fails as a whole
-    // specFileRetries: 1,
-    //
-    // Delay in seconds between the spec file retry attempts
-    // specFileRetriesDelay: 0,
-    //
-    // Whether or not retried specfiles should be retried immediately or deferred to the end of the queue
-    // specFileRetriesDeferred: false,
-    //
-    // Test reporter for stdout.
-    // The only one supported by default is 'dot'
-    // see also: https://webdriver.io/docs/dot-reporter
-    reporters: ['spec'],
-    //
-    // Options to be passed to Mocha.
-    // See the full list at http://mochajs.org/
-    mochaOpts: {
-      ui: 'bdd',
-      timeout: 600000,
-      require: ['node_modules/@babel/register/lib/index.js']
-    },
-    //
-    // =====
-    // Hooks
-    // =====
-    // WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance
-    // it and to build services around it. You can either apply a single function or an array of
-    // methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got
-    // resolved to continue.
-    /**
-     * Gets executed once before all workers get launched.
-     * @param {Object} config wdio configuration object
-     * @param {Array.} capabilities list of capabilities details
-     */
-    //onPrepare: function (config, capabilities) {
-    //},
-    /**
-     * Gets executed before a worker process is spawned and can be used to initialise specific service
-     * for that worker as well as modify runtime environments in an async fashion.
-     * @param  {String} cid      capability id (e.g 0-0)
-     * @param  {[type]} caps     object containing capabilities for session that will be spawn in the worker
-     * @param  {[type]} specs    specs to be run in the worker process
-     * @param  {[type]} args     object that will be merged with the main configuration once worker is initialized
-     * @param  {[type]} execArgv list of string arguments passed to the worker process
-     */
-    onWorkerStart: function (cid: string, caps: any, specs: any, args: any, execArgv: any) {
-      // This is a little 'hacking'
-      // the created 'krux-installer' process generated by
-      // webdriveIO do not properly create the store
-      // so we will run an no-test 'krux-installer'
-      // that create the store, kill it
-      // and then start to test
-      return new Promise(async function(resolve, reject) {
-        if (specs[0].indexOf('000.create-config.spec.mts') !== -1) {
-          const shell = process.env.SHELL
-          debug(`platform: ${process.platform}`)
-          debug(`shell: ${shell}`)
-          debug('INTERMEDIATE RUNNING TO CREATE STORE')
-
-          // Get the Process ID to stop it after exection
-          debug(`exec: ${APP_PATH}`)
-          debug(`args: ${APP_ARGS.join(' ')}`)
-          const app = execFile(APP_PATH, APP_ARGS)
-          debug(`PID ${app.pid}`)
-          
-          // Wait for some time, show the config file path
-          // and stop the process killing it providing it pid.
-          // Linux and darwin have similar ways to do this (`kill` command);
-          // on windows, we will use powershell `Stop-Process -Id` arg
-          // or cmd with `Taskkill /F /PID`
-          if (process.platform !== 'linux' && process.platform !== 'win32' && process.platform !== 'darwin') {
-            const err = new Error(`Not implement for ${process.platform}`)
-            debug(err)
-            reject(err)
-          } else {
-            let store = ''
-            let killCmd = ''
-
-            if (process.platform === 'linux') {
-              store = join(process.env.HOME as string, '.config', 'krux-installer')
-              killCmd = `kill ${app.pid}`
-            }
-          
-            if (process.platform === 'win32') {
-              store = join(process.env.APPDATA as string, 'krux-installer')
-              killCmd = `Taskkill /F /PID ${app.pid}`
-              //killCmd = `Stop-Process -Id ${app.pid}`
-            } 
-          
-            if (process.platform === 'darwin') {
-              store = join(process.env.HOME as string, 'Library', 'Application Support', 'krux-installer')
-              killCmd = `kill ${app.pid}`
-            }
-
-            setTimeout(function () {
-              exec(killCmd, function(err) {
-                if (err) {
-                  debug(err)
-                  reject(err)
-                }
-                debug(`killed process ${app.pid}`)
-                resolve()
-              })
-            }, 10000)
-          }
-        } else {
-          resolve()
-        }
-      })
-    },
-    /**
-     * Gets executed just after a worker process has exited.
-     * @param  {String} cid      capability id (e.g 0-0)
-     * @param  {Number} exitCode 0 - success, 1 - fail
-     * @param  {[type]} specs    specs to be run in the worker process
-     * @param  {Number} retries  number of retries used
-     */
-    // onWorkerEnd: function (cid, exitCode, specs, retries) {
-    // },
-    /**
-     * Gets executed just before initialising the webdriver session and test framework. It allows you
-     * to manipulate configurations depending on the capability or spec.
-     * @param {Object} config wdio configuration object
-     * @param {Array.} capabilities list of capabilities details
-     * @param {Array.} specs List of spec file paths that are to be run
-     * @param {String} cid worker id (e.g. 0-0)
-     */
-    // beforeSession: function (config, capabilities, specs, cid) {
-    // },
-    /**
-     * Gets executed before test execution begins. At this point you can access to all global
-     * variables like `browser`. It is the perfect place to define custom commands.
-     * @param {Array.} capabilities list of capabilities details
-     * @param {Array.} specs        List of spec file paths that are to be run
-     * @param {Object}         browser      instance of created browser/device session
-     */
-    // before: function (capabilities, specs) {
-    // },
-    /**
-     * Runs before a WebdriverIO command gets executed.
-     * @param {String} commandName hook command name
-     * @param {Array} args arguments that command would receive
-     */
-    // beforeCommand: function (commandName, args) {
-    // },
-    /**
-     * Hook that gets executed before the suite starts
-     * @param {Object} suite suite details
-     */
-    // beforeSuite: function (suite) {
-    // },
-    /**
-     * Function to be executed before a test (in Mocha/Jasmine) starts.
-     */
-    // beforeTest: function (test, context) {
-    // },
-    /**
-     * Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling
-     * beforeEach in Mocha)
-     */
-    //beforeHook: function (test, context) {
-    // },
-    /**
-     * Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling
-     * afterEach in Mocha)
-     */
-    // afterHook: function (test, context, { error, result, duration, passed, retries }) {
-    // },
-    /**
-     * Function to be executed after a test (in Mocha/Jasmine only)
-     * @param {Object}  test             test object
-     * @param {Object}  context          scope object the test was executed with
-     * @param {Error}   result.error     error object in case the test fails, otherwise `undefined`
-     * @param {Any}     result.result    return object of test function
-     * @param {Number}  result.duration  duration of test
-     * @param {Boolean} result.passed    true if test has passed, otherwise false
-     * @param {Object}  result.retries   informations to spec related retries, e.g. `{ attempts: 0, limit: 0 }`
-     */
-    // afterTest: function(test, context, { error, result, duration, passed, retries }) {
-    // },
-    /**
-     * Hook that gets executed after the suite has ended
-     * @param {Object} suite suite details
-     */
-    //afterSuite: async function (suite) {
-    //},
-    /**
-     * Runs after a WebdriverIO command gets executed
-     * @param {String} commandName hook command name
-     * @param {Array} args arguments that command would receive
-     * @param {Number} result 0 - command success, 1 - command error
-     * @param {Object} error error object if any
-     */
-    // afterCommand: function (commandName, args, result, error) {
-    // },
-    /**
-     * Gets executed after all tests are done. You still have access to all global variables from
-     * the test.
-     * @param {Number} result 0 - test pass, 1 - test fail
-     * @param {Array.} capabilities list of capabilities details
-     * @param {Array.} specs List of spec file paths that ran
-     */
-    // after: function (result, capabilities, specs) {
-    // },
-    /**
-     * Gets executed right after terminating the webdriver session.
-     * @param {Object} config wdio configuration object
-     * @param {Array.} capabilities list of capabilities details
-     * @param {Array.} specs List of spec file paths that ran
-     */
-    // afterSession: function (config, capabilities, specs) {
-    // },
-    /**
-     * Gets executed after all workers got shut down and the process is about to exit. An error
-     * thrown in the onComplete hook will result in the test run failing.
-     * @param {Object} exitCode 0 - success, 1 - fail
-     * @param {Object} config wdio configuration object
-     * @param {Array.} capabilities list of capabilities details
-     * @param {} results object containing test results
-     */
-    // eslint-disable-next-line no-unused-vars
-    onComplete: async function(exitCode: number, config: Object, capabilities: Object[], results: Object) {
-
-      async function removing(dir: string) {
-        debug(`Cleaning ${dir}`)
-        try {
-          await rimraf(dir, { preserveRoot: false })
-          debug(`${dir} removed`)
-        } catch (error) {
-          debug(error)
-        }
-      }
-
-      const tmp = tmpdir()
-      const yarnDir = join(tmp, 'yarn--*')
-      const lightDir = join(tmp, 'lighthouse.*')
-      const yarns = await glob(yarnDir)
-      const lights = await glob(lightDir)
-      yarns.map(removing)
-      lights.map(removing)
-    },
-    /**
-    * Gets executed when a refresh happens.
-    * @param {String} oldSessionId session ID of the old session
-    * @param {String} newSessionId session ID of the new session
-    */
-    // onReload: function(oldSessionId, newSessionId) {
-    // }
-}