diff --git a/INSTALL.md b/INSTALL.md index 50fa0cf76f..2c60ecafd2 100755 --- a/INSTALL.md +++ b/INSTALL.md @@ -135,10 +135,11 @@ A desktop shortcut can be added to easily launch straight into the faceswap GUI: ## Updating faceswap It's good to keep faceswap up to date as new features are added and bugs are fixed. To do so: +- If using the GUI you can go to the Tools Menu and select "Check for Updates...". This will update Faceswap to the latest code and update your dependencies. - If you are not already in your virtual environment follow [these steps](#entering-your-virtual-environment) - Enter the faceswap folder: `cd faceswap` - Enter the following `git pull --all` -- Once the latest version has downloaded, make sure your requirements are up to date: `pip install --upgrade -r requirements.txt` +- Once the latest version has downloaded, make sure your dependencies are up to date. There is a script to help with this: `python update_deps.py` # General Install Guide ## Installing dependencies diff --git a/lib/gui/menu.py b/lib/gui/menu.py index c1336485a5..b79de334ec 100644 --- a/lib/gui/menu.py +++ b/lib/gui/menu.py @@ -13,6 +13,7 @@ from lib.multithreading import MultiThread from lib.Serializer import JSONSerializer +import update_deps from .utils import get_config from .popup_configure import popup_config @@ -188,16 +189,20 @@ def output_sysinfo(self): def update(self): """ Check for updates and clone repo """ - logger.debug("Updating Faceswap") + logger.debug("Updating Faceswap...") self.root.config(cursor="watch") - self.clear_console() encoding = locale.getpreferredencoding() logger.debug("Encoding: %s", encoding) + success = False if self.check_for_updates(encoding): - self.do_update(encoding) + success = self.do_update(encoding) + update_deps.main(logger=logger) + if success: + logger.info("Please restart Faceswap to complete the update.") self.root.config(cursor="") - def check_for_updates(self, encoding): + @staticmethod + def check_for_updates(encoding): """ Check whether an update is required """ # Do the check logger.info("Checking for updates...") @@ -227,7 +232,6 @@ def check_for_updates(self, encoding): if "have diverged" in line.lower(): msg = "Your branch has diverged from the remote repo. Not updating" break - self.clear_console() if not update: logger.info(msg) logger.debug("Checked for update. Update required: %s", update) @@ -249,7 +253,8 @@ def do_update(encoding): retcode = cmd.poll() logger.debug("'%s' returncode: %s", gitcmd, retcode) if retcode != 0: - msg = "An error occurred during update. return code: {}".format(retcode) + logger.info("An error occurred during update. return code: %s", retcode) + retval = False else: - msg = "Please restart Faceswap to complete the update." - logger.info(msg) + retval = True + return retval diff --git a/setup.py b/setup.py index 2419acdcad..3f0f52b164 100755 --- a/setup.py +++ b/setup.py @@ -540,6 +540,7 @@ def check_missing_dep(self): if self.env.enable_cuda and self.env.is_macos: self.env.required_packages.extend(self.env.macos_required_packages) for pkg in self.env.required_packages: + pkg = self.check_os_requirements(pkg) key = pkg.split("==")[0] if key not in self.env.installed_packages: self.env.missing_packages.append(pkg) @@ -550,6 +551,19 @@ def check_missing_dep(self): self.env.missing_packages.append(pkg) continue + @staticmethod + def check_os_requirements(package): + """ Check that the required package is required for this OS """ + if ";" not in package and "sys_platform" not in package: + return package + package = "".join(package.split()) + pkg, tags = package.split(";") + tags = tags.split("==") + sys_platform = tags[tags.index("sys_platform") + 1].replace('"', "").replace("'", "") + if sys_platform == sys.platform: + return pkg + return None + def check_conda_missing_dep(self): """ Check for conda missing dependencies """ if not self.env.is_conda: diff --git a/update_deps.py b/update_deps.py new file mode 100644 index 0000000000..9843e6b855 --- /dev/null +++ b/update_deps.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +""" Installs any required third party libs for faceswap.py + + Checks for installed Conda / Pip packages and updates accordingly +""" + +import locale +import os +import re +import sys +import ctypes + +from subprocess import CalledProcessError, run, PIPE, Popen + +_LOGGER = None + + +class Environment(): + """ Hold information about the running environment """ + def __init__(self): + self.is_conda = "conda" in sys.version.lower() + self.encoding = locale.getpreferredencoding() + self.is_admin = self.get_admin_status() + self.is_virtualenv = self.get_virtualenv + required_packages = self.get_required_packages() + self.installed_packages = self.get_installed_packages() + self.get_installed_conda_packages() + self.packages_to_install = self.get_packages_to_install(required_packages) + + @staticmethod + def get_admin_status(): + """ Check whether user is admin """ + try: + retval = os.getuid() == 0 + except AttributeError: + retval = ctypes.windll.shell32.IsUserAnAdmin() != 0 + return retval + + def get_virtualenv(self): + """ Check whether this is a virtual environment """ + if not self.is_conda: + retval = (hasattr(sys, "real_prefix") or + (hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix)) + else: + prefix = os.path.dirname(sys.prefix) + retval = (os.path.basename(prefix) == "envs") + return retval + + @staticmethod + def get_required_packages(): + """ Load requirements list """ + packages = list() + pypath = os.path.dirname(os.path.realpath(__file__)) + requirements_file = os.path.join(pypath, "requirements.txt") + with open(requirements_file) as req: + for package in req.readlines(): + package = package.strip() + if package and (not package.startswith("#")): + packages.append(package) + return packages + + def get_installed_packages(self): + """ Get currently installed packages """ + installed_packages = dict() + chk = Popen("\"{}\" -m pip freeze".format(sys.executable), + shell=True, stdout=PIPE) + installed = chk.communicate()[0].decode(self.encoding).splitlines() + + for pkg in installed: + if "==" not in pkg: + continue + item = pkg.split("==") + installed_packages[item[0]] = item[1] + return installed_packages + + def get_installed_conda_packages(self): + """ Get currently installed conda packages """ + if not self.is_conda: + return + chk = os.popen("conda list").read() + installed = [re.sub(" +", " ", line.strip()) + for line in chk.splitlines() if not line.startswith("#")] + for pkg in installed: + item = pkg.split(" ") + self.installed_packages[item[0]] = item[1] + + def get_packages_to_install(self, required_packages): + """ Get packages which need installing, upgrading or downgrading """ + to_install = list() + for pkg in required_packages: + pkg = self.check_os_requirement(pkg) + if pkg is None: + continue + key = pkg.split("==")[0] + if key not in self.installed_packages: + to_install.append(pkg) + else: + if (len(pkg.split("==")) > 1 and + pkg.split("==")[1] != self.installed_packages.get(key)): + to_install.append(pkg) + return to_install + + @staticmethod + def check_os_requirement(package): + """ Check whether this package is required for this OS """ + if ";" not in package and "sys_platform" not in package: + return package + package = "".join(package.split()) + pkg, tags = package.split(";") + tags = tags.split("==") + sys_platform = tags[tags.index("sys_platform") + 1].replace('"', "").replace("'", "") + if sys_platform == sys.platform: + return pkg + return None + + +class Installer(): + """ Install packages through Conda or Pip """ + def __init__(self, environment): + self.packages = environment.packages_to_install + self.env = environment + self.install() + + def install(self): + """ Install required pip packages """ + success = True + for pkg in self.packages: + output("Installing {}".format(pkg)) + if self.env.is_conda and self.conda_install(pkg): + continue + if not self.pip_install(pkg): + success = False + if not success: + output("There were problems updating one or more dependencies.") + else: + output("Dependencies succesfully updated.") + + @staticmethod + def conda_install(package): + """ Install a conda package """ + success = True + condaexe = ["conda", "install", "-y", package] + try: + with open(os.devnull, "w") as devnull: + run(condaexe, stdout=devnull, stderr=devnull, check=True) + except CalledProcessError: + output("{} not available in Conda. Installing with pip...".format(package)) + success = False + return success + + def pip_install(self, package): + """ Install a pip package """ + success = True + pipexe = [sys.executable, "-m", "pip"] + # hide info/warning and fix cache hang + pipexe.extend(["install", "-qq", "--no-cache-dir"]) + # install as user to solve perm restriction + if not self.env.is_admin and not self.env.is_virtualenv: + pipexe.append("--user") + pipexe.append(package) + try: + run(pipexe, check=True) + except CalledProcessError: + output("Couldn't install {}. Please install this package manually".format(package)) + success = False + return success + + +def output(msg): + """ Output to print or logger """ + if _LOGGER is not None: + _LOGGER.info(msg) + else: + print(msg) + + +def main(logger=None): + """ Check for and update dependencies """ + if logger is not None: + global _LOGGER # pylint:disable=global-statement + _LOGGER = logger + output("Updating Dependencies...") + update = Environment() + packages = update.packages_to_install + if not packages: + output("All Dependencies are up to date") + else: + Installer(update) + + +if __name__ == "__main__": + main()