Repository: Cog-Creators/Red-DiscordBot

Issue:
[Downloader] Add a command to redownload installed cog dependencies
# Feature request

<!-- This template is for feature requests. Please fill out the following: -->


#### Select the type of feature you are requesting:

<!-- To check a box, replace the space between the [] with a x -->

- [ ] Cog
- [X] Command
- [X] API functionality

#### Describe your requested feature

Add a core command to be able to reinstall cog dependencies, be this to update existing dependencies or Download missing ones, this would be applicable for when people restore their bot having a command that can get all missing dependencies would make the end user life easier

an example of how this is applicable is changing python version (Going from 3.7 to 3.8 or 3.8 to 3.7) would also allow increasing the minimum python version significantly easier as you could just run this command after upgrading python to get all cog dependencies.



Discussion:

Resolving patch:
diff --git a/redbot/cogs/downloader/downloader.py b/redbot/cogs/downloader/downloader.py
--- a/redbot/cogs/downloader/downloader.py
+++ b/redbot/cogs/downloader/downloader.py
@@ -50,13 +50,18 @@ def __init__(self, bot: Red):
         self.SHAREDLIB_PATH = self.LIB_PATH / "cog_shared"
         self.SHAREDLIB_INIT = self.SHAREDLIB_PATH / "__init__.py"
 
+        self._create_lib_folder()
+
+        self._repo_manager = RepoManager()
+
+    def _create_lib_folder(self, *, remove_first: bool = False) -> None:
+        if remove_first:
+            shutil.rmtree(str(self.LIB_PATH))
         self.SHAREDLIB_PATH.mkdir(parents=True, exist_ok=True)
         if not self.SHAREDLIB_INIT.exists():
             with self.SHAREDLIB_INIT.open(mode="w", encoding="utf-8") as _:
                 pass
 
-        self._repo_manager = RepoManager()
-
     async def initialize(self) -> None:
         await self._repo_manager.initialize()
         await self._maybe_update_config()
@@ -553,6 +558,59 @@ async def cog(self, ctx: commands.Context) -> None:
         """Cog installation management commands."""
         pass
 
+    @cog.command(name="reinstallreqs")
+    async def _cog_reinstallreqs(self, ctx: commands.Context) -> None:
+        """
+        This command will reinstall cog requirements and shared libraries for all installed cogs.
+
+        Red might ask user to use this when it clears contents of lib folder
+        because of change in minor version of Python.
+        """
+        async with ctx.typing():
+            self._create_lib_folder(remove_first=True)
+            installed_cogs = await self.installed_cogs()
+            cogs = []
+            repos = set()
+            for cog in installed_cogs:
+                if cog.repo is None:
+                    continue
+                repos.add(cog.repo)
+                cogs.append(cog)
+            failed_reqs = await self._install_requirements(cogs)
+            all_installed_libs: List[InstalledModule] = []
+            all_failed_libs: List[Installable] = []
+            for repo in repos:
+                installed_libs, failed_libs = await repo.install_libraries(
+                    target_dir=self.SHAREDLIB_PATH, req_target_dir=self.LIB_PATH
+                )
+                all_installed_libs += installed_libs
+                all_failed_libs += failed_libs
+        message = ""
+        if failed_reqs:
+            message += _("Failed to install requirements: ") + humanize_list(
+                tuple(map(inline, failed_reqs))
+            )
+        if all_failed_libs:
+            libnames = [lib.name for lib in failed_libs]
+            message += _("\nFailed to install shared libraries: ") + humanize_list(
+                tuple(map(inline, libnames))
+            )
+        if message:
+            await ctx.send(
+                _(
+                    "Cog requirements and shared libraries for all installed cogs"
+                    " have been reinstalled but there were some errors:\n"
+                )
+                + message
+            )
+        else:
+            await ctx.send(
+                _(
+                    "Cog requirements and shared libraries"
+                    " for all installed cogs have been reinstalled."
+                )
+            )
+
     @cog.command(name="install", usage="<repo_name> <cogs>")
     async def _cog_install(self, ctx: commands.Context, repo: Repo, *cog_names: str) -> None:
         """Install a cog from the given repo."""
diff --git a/redbot/core/bot.py b/redbot/core/bot.py
--- a/redbot/core/bot.py
+++ b/redbot/core/bot.py
@@ -2,6 +2,8 @@
 import inspect
 import logging
 import os
+import platform
+import shutil
 import sys
 from collections import namedtuple
 from datetime import datetime
@@ -17,6 +19,7 @@
 from . import Config, i18n, commands, errors, drivers, modlog, bank
 from .cog_manager import CogManager, CogManagerUI
 from .core_commands import license_info_command, Core
+from .data_manager import cog_data_path
 from .dev_commands import Dev
 from .events import init_events
 from .global_checks import init_global_checks
@@ -79,6 +82,9 @@ def __init__(self, *args, cli_flags=None, bot_dir: Path = Path.cwd(), **kwargs):
             disabled_command_msg="That command is disabled.",
             extra_owner_destinations=[],
             owner_opt_out_list=[],
+            last_system_info__python_version=[3, 7],
+            last_system_info__machine=None,
+            last_system_info__system=None,
             schema_version=0,
         )
 
@@ -413,11 +419,70 @@ async def pre_flight(self, cli_flags):
 
         packages = []
 
-        if cli_flags.no_cogs is False:
-            packages.extend(await self._config.packages())
+        last_system_info = await self._config.last_system_info()
 
-        if cli_flags.load_cogs:
-            packages.extend(cli_flags.load_cogs)
+        async def notify_owners(content: str) -> None:
+            destinations = await self.get_owner_notification_destinations()
+            for destination in destinations:
+                prefixes = await self.get_valid_prefixes(getattr(destination, "guild", None))
+                prefix = prefixes[0]
+                try:
+                    await destination.send(content.format(prefix=prefix))
+                except Exception as _exc:
+                    log.exception(
+                        f"I could not send an owner notification to ({destination.id}){destination}"
+                    )
+
+        ver_info = list(sys.version_info[:2])
+        python_version_changed = False
+        LIB_PATH = cog_data_path(raw_name="Downloader") / "lib"
+        if ver_info != last_system_info["python_version"]:
+            await self._config.last_system_info.python_version.set(ver_info)
+            if any(LIB_PATH.iterdir()):
+                shutil.rmtree(str(LIB_PATH))
+                LIB_PATH.mkdir()
+                self.loop.create_task(
+                    notify_owners(
+                        "We detected a change in minor Python version"
+                        " and cleared packages in lib folder.\n"
+                        "The instance was started with no cogs, please load Downloader"
+                        " and use `{prefix}cog reinstallreqs` to regenerate lib folder."
+                        " After that, restart the bot to get"
+                        " all of your previously loaded cogs loaded again."
+                    )
+                )
+                python_version_changed = True
+        else:
+            if cli_flags.no_cogs is False:
+                packages.extend(await self._config.packages())
+
+            if cli_flags.load_cogs:
+                packages.extend(cli_flags.load_cogs)
+
+        system_changed = False
+        machine = platform.machine()
+        system = platform.system()
+        if last_system_info["machine"] is None:
+            await self._config.last_system_info.machine.set(machine)
+        elif last_system_info["machine"] != machine:
+            await self._config.last_system_info.machine.set(machine)
+            system_changed = True
+
+        if last_system_info["system"] is None:
+            await self._config.last_system_info.system.set(system)
+        elif last_system_info["system"] != system:
+            await self._config.last_system_info.system.set(system)
+            system_changed = True
+
+        if system_changed and not python_version_changed:
+            self.loop.create_task(
+                notify_owners(
+                    "We detected a possible change in machine's operating system"
+                    " or architecture. You might need to regenerate your lib folder"
+                    " if 3rd-party cogs stop working properly.\n"
+                    "To regenerate lib folder, load Downloader and use `{prefix}cog reinstallreqs`."
+                )
+            )
 
         if packages:
             # Load permissions first, for security reasons


Test patch: