mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2026-05-15 04:21:47 -04:00
216 lines
7.7 KiB
Python
216 lines
7.7 KiB
Python
import asyncio
|
|
import json
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
from typing import Final, Optional, Tuple
|
|
|
|
import click
|
|
from packaging.version import Version
|
|
from rich.text import Text
|
|
|
|
from redbot._update import cog_compatibility_checker, common
|
|
from redbot._update.cog_compatibility_checker import CompatibilitySummary
|
|
from redbot.core import _drivers
|
|
from redbot.core._cli import asyncio_run
|
|
from redbot.core.utils._internal_utils import fetch_latest_red_version
|
|
|
|
from . import arg_names
|
|
|
|
|
|
EXIT_INSTANCE_SITE_PREFIX_MISMATCH: Final = 4
|
|
EXIT_INSTANCE_BACKEND_UNSUPPORTED: Final = 5
|
|
CMD_NAME: Final = "check-cog-compatibility"
|
|
_COMPATIBILITY_RESULTS_ENV_VAR = "_RED_UPDATE_COMPATIBILITY_RESULTS_FILE"
|
|
|
|
|
|
@click.command(CMD_NAME)
|
|
@click.argument(
|
|
"instances",
|
|
nargs=-1,
|
|
type=click.Choice(common.INSTANCE_LIST),
|
|
default=None,
|
|
metavar="[INSTANCE_NAME]",
|
|
)
|
|
@click.option(
|
|
arg_names.RED_VERSION,
|
|
type=common.VersionParamType(),
|
|
default=None,
|
|
help="The Red version to check cog compatibility for."
|
|
" If not provided, the information about latest available version will be fetched"
|
|
" and the command will check whether installed cogs support that version.\n"
|
|
"If this option is provided, --python-version also has to be provided.",
|
|
)
|
|
@click.option(
|
|
arg_names.PYTHON_VERSION,
|
|
type=common.VersionParamType(),
|
|
default=None,
|
|
help="The Python version to check cog compatibility for."
|
|
" If not provided, the command will either use the current interpreter's version or,"
|
|
" if that version is not compatible with the latest Red version, it will try to"
|
|
" find the latest available CPython interpreter on the system and will check whether"
|
|
" installed cogs support it.\n"
|
|
"If this option is provided, --red-version also has to be provided.",
|
|
)
|
|
@click.pass_context
|
|
def check_cog_compatibility(
|
|
ctx: click.Context,
|
|
instances: Tuple[str, ...],
|
|
red_version: Optional[Version],
|
|
python_version: Optional[Version],
|
|
) -> None:
|
|
"""
|
|
Check if the installed cogs are compatible with the given version.
|
|
"""
|
|
if (red_version, python_version).count(None) == 1:
|
|
raise click.BadParameter(
|
|
"Either both --red-version and --python-version options"
|
|
" have to be specified or neither.",
|
|
param_hint=[arg_names.RED_VERSION, arg_names.PYTHON_VERSION],
|
|
)
|
|
|
|
asyncio_run(
|
|
_check_cog_compatibility_command_impl(
|
|
red_version=red_version,
|
|
python_version=python_version,
|
|
instances=instances,
|
|
ignore_prefix=ctx.obj["IGNORE_PREFIX"],
|
|
)
|
|
)
|
|
|
|
|
|
async def _check_cog_compatibility_command_impl(
|
|
*,
|
|
red_version: Optional[Version],
|
|
python_version: Optional[Version],
|
|
instances: Tuple[str, ...] = (),
|
|
ignore_prefix: bool = False,
|
|
) -> None:
|
|
console = common.get_console()
|
|
if red_version is None or python_version is None:
|
|
with console.status("Checking latest version..."):
|
|
latest = await fetch_latest_red_version()
|
|
red_version = latest.version
|
|
|
|
python_version = Version(".".join(map(str, sys.version_info[:3])))
|
|
if python_version not in latest.requires_python:
|
|
interpreters = common.search_for_interpreters(latest.requires_python)
|
|
_, python_version, _ = interpreters[0]
|
|
|
|
if len(instances) == 1:
|
|
results_file = os.getenv(_COMPATIBILITY_RESULTS_ENV_VAR, "")
|
|
try:
|
|
results = await cog_compatibility_checker.check_instance(
|
|
instances[0],
|
|
latest_version=red_version,
|
|
interpreter_version=python_version,
|
|
ignore_prefix=ignore_prefix,
|
|
)
|
|
except _drivers.MissingExtraRequirements:
|
|
if not results_file:
|
|
common.print_with_prefix_column(
|
|
common.ICON_ERROR,
|
|
Text(instances[0], style="bold"),
|
|
" instance could not be checked as it uses a storage backend"
|
|
" that is not supported by the current Red installation"
|
|
" (some requirements are missing).",
|
|
)
|
|
raise SystemExit(EXIT_INSTANCE_BACKEND_UNSUPPORTED)
|
|
except cog_compatibility_checker.InstanceSitePrefixMismatchError as exc:
|
|
if not results_file:
|
|
common.print_with_prefix_column(
|
|
common.ICON_ERROR,
|
|
Text(exc.instance_name, style="bold"),
|
|
" instance could not be checked as it is a part of"
|
|
" a different Python installation and/or virtual environment.",
|
|
)
|
|
raise SystemExit(EXIT_INSTANCE_SITE_PREFIX_MISMATCH)
|
|
if results_file:
|
|
with open(results_file, "w", encoding="utf-8") as fp:
|
|
json.dump(results.to_json_dict(), fp)
|
|
return
|
|
|
|
if not instances:
|
|
instances = tuple(common.INSTANCE_LIST)
|
|
checked_instances = []
|
|
for instance_name in instances:
|
|
exit_code, _, _ = await call(
|
|
instance_name,
|
|
red_version=red_version,
|
|
python_version=python_version,
|
|
ignore_prefix=ignore_prefix,
|
|
)
|
|
if exit_code != EXIT_INSTANCE_SITE_PREFIX_MISMATCH:
|
|
if exit_code:
|
|
raise SystemExit(exit_code)
|
|
checked_instances.append(instance_name)
|
|
|
|
if not checked_instances:
|
|
common.print_with_prefix_column(
|
|
common.ICON_ERROR, "There were no instances to check cog compatibility for."
|
|
)
|
|
raise SystemExit(1)
|
|
|
|
|
|
async def call(
|
|
instance_name: str,
|
|
*,
|
|
red_version: Version,
|
|
python_version: Version,
|
|
ignore_prefix: bool = False,
|
|
return_results: bool = False,
|
|
stdout: Optional[int] = None,
|
|
) -> Tuple[int, Optional[str], Optional[CompatibilitySummary]]:
|
|
debug_args = (arg_names.DEBUG,) * common.get_log_cli_level()
|
|
args = [
|
|
"-m",
|
|
"redbot._update",
|
|
*debug_args,
|
|
CMD_NAME,
|
|
instance_name,
|
|
arg_names.RED_VERSION,
|
|
str(red_version),
|
|
arg_names.PYTHON_VERSION,
|
|
str(python_version),
|
|
]
|
|
if ignore_prefix:
|
|
args.append(arg_names.CHECK_OTHER_PYTHON_INSTALLS)
|
|
env = os.environ.copy()
|
|
|
|
# terminal woes
|
|
console = common.get_console()
|
|
if console.is_terminal:
|
|
env["TTY_COMPATIBLE"] = "1"
|
|
# Rich only checks stdout for Windows console features:
|
|
# https://github.com/Textualize/rich/blob/fc41075a3206d2a5fd846c6f41c4d2becab814fa/rich/_windows.py#L46
|
|
env[common.INTERNAL_LEGACY_WINDOWS_ENV_VAR] = "1" if console.legacy_windows else "0"
|
|
else:
|
|
# Rich does not set legacy_windows correctly when is_terminal is False
|
|
# https://github.com/Textualize/rich/issues/3647
|
|
env[common.INTERNAL_LEGACY_WINDOWS_ENV_VAR] = "0"
|
|
env["PYTHONIOENCODING"] = sys.stdout.encoding
|
|
|
|
results = None
|
|
results_file = None
|
|
if return_results:
|
|
results_file = tempfile.NamedTemporaryFile(delete=False)
|
|
try:
|
|
if results_file is not None:
|
|
results_file.close()
|
|
env[_COMPATIBILITY_RESULTS_ENV_VAR] = str(results_file.name)
|
|
|
|
proc = await asyncio.create_subprocess_exec(sys.executable, *args, env=env, stdout=stdout)
|
|
stdout_data, _ = await proc.communicate()
|
|
decoded_stdout = None
|
|
if stdout_data is not None:
|
|
decoded_stdout = stdout_data.decode()
|
|
exit_code = await proc.wait()
|
|
if not exit_code and results_file is not None:
|
|
with open(results_file.name, encoding="utf-8") as fp:
|
|
results = CompatibilitySummary.from_json_dict(json.load(fp))
|
|
finally:
|
|
if results_file is not None:
|
|
os.remove(results_file.name)
|
|
|
|
return exit_code, decoded_stdout, results
|