mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-07 11:48:55 -05:00
Red's launcher / Downloader reqs autoinstall (#552)
This commit is contained in:
parent
04b00b7726
commit
c987a89e9c
@ -12,6 +12,7 @@ import discord
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from time import time
|
from time import time
|
||||||
|
from importlib.util import find_spec
|
||||||
|
|
||||||
NUM_THREADS = 4
|
NUM_THREADS = 4
|
||||||
REPO_NONEX = 0x1
|
REPO_NONEX = 0x1
|
||||||
@ -27,6 +28,10 @@ class CloningError(UpdateError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RequirementFail(UpdateError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Downloader:
|
class Downloader:
|
||||||
"""Cog downloader/installer."""
|
"""Cog downloader/installer."""
|
||||||
|
|
||||||
@ -207,6 +212,7 @@ class Downloader:
|
|||||||
updated_cogs = []
|
updated_cogs = []
|
||||||
new_cogs = []
|
new_cogs = []
|
||||||
deleted_cogs = []
|
deleted_cogs = []
|
||||||
|
failed_cogs = []
|
||||||
error_repos = {}
|
error_repos = {}
|
||||||
installed_updated_cogs = []
|
installed_updated_cogs = []
|
||||||
|
|
||||||
@ -233,6 +239,21 @@ class Downloader:
|
|||||||
msg = await self._robust_edit(msg, base_msg + status)
|
msg = await self._robust_edit(msg, base_msg + status)
|
||||||
status = 'done. '
|
status = 'done. '
|
||||||
|
|
||||||
|
for t in updated_cogs:
|
||||||
|
repo, cog, _ = t
|
||||||
|
if self.repos[repo][cog]['INSTALLED']:
|
||||||
|
try:
|
||||||
|
await self.install(repo, cog,
|
||||||
|
no_install_on_reqs_fail=False)
|
||||||
|
except RequirementFail:
|
||||||
|
failed_cogs.append(t)
|
||||||
|
else:
|
||||||
|
installed_updated_cogs.append(t)
|
||||||
|
|
||||||
|
for t in updated_cogs.copy():
|
||||||
|
if t in failed_cogs:
|
||||||
|
updated_cogs.remove(t)
|
||||||
|
|
||||||
if not any(self.repos[repo][cog]['INSTALLED'] for
|
if not any(self.repos[repo][cog]['INSTALLED'] for
|
||||||
repo, cog, _ in updated_cogs):
|
repo, cog, _ in updated_cogs):
|
||||||
status += ' No updates to apply. '
|
status += ' No updates to apply. '
|
||||||
@ -246,6 +267,10 @@ class Downloader:
|
|||||||
if updated_cogs:
|
if updated_cogs:
|
||||||
status += '\nUpdated cogs: ' \
|
status += '\nUpdated cogs: ' \
|
||||||
+ ', '.join('%s/%s' % c[:2] for c in updated_cogs) + '.'
|
+ ', '.join('%s/%s' % c[:2] for c in updated_cogs) + '.'
|
||||||
|
if failed_cogs:
|
||||||
|
status += '\nCogs that got new requirements which have ' + \
|
||||||
|
'failed to install: ' + \
|
||||||
|
', '.join('%s/%s' % c[:2] for c in failed_cogs) + '.'
|
||||||
if error_repos:
|
if error_repos:
|
||||||
status += '\nThe following repos failed to update: '
|
status += '\nThe following repos failed to update: '
|
||||||
for n, what in error_repos.items():
|
for n, what in error_repos.items():
|
||||||
@ -253,15 +278,6 @@ class Downloader:
|
|||||||
|
|
||||||
msg = await self._robust_edit(msg, base_msg + status)
|
msg = await self._robust_edit(msg, base_msg + status)
|
||||||
|
|
||||||
registry = dataIO.load_json("data/red/cogs.json")
|
|
||||||
|
|
||||||
for t in updated_cogs:
|
|
||||||
repo, cog, _ = t
|
|
||||||
if (self.repos[repo][cog]['INSTALLED'] and
|
|
||||||
registry.get('cogs.' + cog, False)):
|
|
||||||
installed_updated_cogs.append(t)
|
|
||||||
await self.install(repo, cog)
|
|
||||||
|
|
||||||
if not installed_updated_cogs:
|
if not installed_updated_cogs:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -280,9 +296,12 @@ class Downloader:
|
|||||||
await self.bot.say("Ok then, you can reload cogs with"
|
await self.bot.say("Ok then, you can reload cogs with"
|
||||||
" `{}reload <cog_name>`".format(ctx.prefix))
|
" `{}reload <cog_name>`".format(ctx.prefix))
|
||||||
elif answer.content.lower().strip() == "yes":
|
elif answer.content.lower().strip() == "yes":
|
||||||
|
registry = dataIO.load_json("data/red/cogs.json")
|
||||||
update_list = []
|
update_list = []
|
||||||
fail_list = []
|
fail_list = []
|
||||||
for repo, cog, _ in installed_updated_cogs:
|
for repo, cog, _ in installed_updated_cogs:
|
||||||
|
if not registry.get('cogs.' + cog, False):
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
self.bot.unload_extension("cogs." + cog)
|
self.bot.unload_extension("cogs." + cog)
|
||||||
self.bot.load_extension("cogs." + cog)
|
self.bot.load_extension("cogs." + cog)
|
||||||
@ -342,8 +361,14 @@ class Downloader:
|
|||||||
if cog not in self.repos[repo_name]:
|
if cog not in self.repos[repo_name]:
|
||||||
await self.bot.say("That cog isn't available from that repo.")
|
await self.bot.say("That cog isn't available from that repo.")
|
||||||
return
|
return
|
||||||
install_cog = await self.install(repo_name, cog)
|
|
||||||
data = self.get_info_data(repo_name, cog)
|
data = self.get_info_data(repo_name, cog)
|
||||||
|
try:
|
||||||
|
install_cog = await self.install(repo_name, cog, notify_reqs=True)
|
||||||
|
except RequirementFail:
|
||||||
|
await self.bot.say("That cog has requirements that I could not "
|
||||||
|
"install. Check the console for more "
|
||||||
|
"informations.")
|
||||||
|
return
|
||||||
if data is not None:
|
if data is not None:
|
||||||
install_msg = data.get("INSTALL_MSG", None)
|
install_msg = data.get("INSTALL_MSG", None)
|
||||||
if install_msg:
|
if install_msg:
|
||||||
@ -368,13 +393,39 @@ class Downloader:
|
|||||||
await self.bot.say("That cog doesn't exist. Use cog list to see"
|
await self.bot.say("That cog doesn't exist. Use cog list to see"
|
||||||
" the full list.")
|
" the full list.")
|
||||||
|
|
||||||
async def install(self, repo_name, cog):
|
async def install(self, repo_name, cog, *, notify_reqs=False,
|
||||||
|
no_install_on_reqs_fail=True):
|
||||||
|
# 'no_install_on_reqs_fail' will make the cog get installed anyway
|
||||||
|
# on requirements installation fail. This is necessary because due to
|
||||||
|
# how 'cog update' works right now, the user would have no way to
|
||||||
|
# reupdate the cog if the update fails, since 'cog update' only
|
||||||
|
# updates the cogs that get a new commit.
|
||||||
|
# This is not a great way to deal with the problem and a cog update
|
||||||
|
# rework would probably be the best course of action.
|
||||||
|
reqs_failed = False
|
||||||
if cog.endswith('.py'):
|
if cog.endswith('.py'):
|
||||||
cog = cog[:-3]
|
cog = cog[:-3]
|
||||||
|
|
||||||
path = self.repos[repo_name][cog]['file']
|
path = self.repos[repo_name][cog]['file']
|
||||||
cog_folder_path = self.repos[repo_name][cog]['folder']
|
cog_folder_path = self.repos[repo_name][cog]['folder']
|
||||||
cog_data_path = os.path.join(cog_folder_path, 'data')
|
cog_data_path = os.path.join(cog_folder_path, 'data')
|
||||||
|
data = self.get_info_data(repo_name, cog)
|
||||||
|
requirements = data.get("REQUIREMENTS", [])
|
||||||
|
|
||||||
|
requirements = [r for r in requirements
|
||||||
|
if not self.is_lib_installed(r)]
|
||||||
|
|
||||||
|
if requirements and notify_reqs:
|
||||||
|
await self.bot.say("Installing cog's requirements...")
|
||||||
|
|
||||||
|
for requirement in requirements:
|
||||||
|
if not self.is_lib_installed(requirement):
|
||||||
|
success = await self.bot.pip_install(requirement)
|
||||||
|
if not success:
|
||||||
|
if no_install_on_reqs_fail:
|
||||||
|
raise RequirementFail()
|
||||||
|
else:
|
||||||
|
reqs_failed = True
|
||||||
|
|
||||||
to_path = os.path.join("cogs/", cog + ".py")
|
to_path = os.path.join("cogs/", cog + ".py")
|
||||||
|
|
||||||
@ -387,7 +438,10 @@ class Downloader:
|
|||||||
os.path.join('data/', cog))
|
os.path.join('data/', cog))
|
||||||
self.repos[repo_name][cog]['INSTALLED'] = True
|
self.repos[repo_name][cog]['INSTALLED'] = True
|
||||||
self.save_repos()
|
self.save_repos()
|
||||||
return True
|
if not reqs_failed:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
raise RequirementFail()
|
||||||
|
|
||||||
def get_info_data(self, repo_name, cog=None):
|
def get_info_data(self, repo_name, cog=None):
|
||||||
if cog is not None:
|
if cog is not None:
|
||||||
@ -440,6 +494,9 @@ class Downloader:
|
|||||||
git_name = splitted[-1]
|
git_name = splitted[-1]
|
||||||
return git_name[:-4]
|
return git_name[:-4]
|
||||||
|
|
||||||
|
def is_lib_installed(self, name):
|
||||||
|
return bool(find_spec(name))
|
||||||
|
|
||||||
def _do_first_run(self):
|
def _do_first_run(self):
|
||||||
invalid = []
|
invalid = []
|
||||||
save = False
|
save = False
|
||||||
|
|||||||
@ -490,9 +490,31 @@ class Owner:
|
|||||||
|
|
||||||
@commands.command()
|
@commands.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
async def shutdown(self):
|
async def shutdown(self, silently : bool=False):
|
||||||
"""Shuts down Red"""
|
"""Shuts down Red"""
|
||||||
await self.bot.logout()
|
wave = "\N{WAVING HAND SIGN}"
|
||||||
|
skin = "\N{EMOJI MODIFIER FITZPATRICK TYPE-3}"
|
||||||
|
try: # We don't want missing perms to stop our shutdown
|
||||||
|
if not silently:
|
||||||
|
await self.bot.say("Shutting down... " + wave + skin)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
await self.bot.shutdown()
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
@checks.is_owner()
|
||||||
|
async def restart(self, silently : bool=False):
|
||||||
|
"""Attempts to restart Red
|
||||||
|
|
||||||
|
Makes Red quit with exit code 26
|
||||||
|
The restart is not guaranteed: it must be dealt
|
||||||
|
with by the process manager in use"""
|
||||||
|
try:
|
||||||
|
if not silently:
|
||||||
|
await self.bot.say("Restarting...")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
await self.bot.shutdown(restart=True)
|
||||||
|
|
||||||
@commands.group(name="command", pass_context=True)
|
@commands.group(name="command", pass_context=True)
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
@echo off
|
|
||||||
pushd %~dp0
|
|
||||||
IF "%PROCESSOR_ARCHITECTURE%"=="x86" (GOTO 32bit) else (GOTO 64bit)
|
|
||||||
echo Couldn't detect system bitness.
|
|
||||||
PAUSE
|
|
||||||
GOTO end
|
|
||||||
|
|
||||||
:32bit
|
|
||||||
echo Windows 32bit detected. You'll need to manually install ffmpeg. Once you press enter I'm going to open a web page.
|
|
||||||
echo Keep following the instructions.
|
|
||||||
PAUSE
|
|
||||||
start "" https://ffmpeg.zeranoe.com/builds/
|
|
||||||
echo Download "FFmpeg 32-bit Static"
|
|
||||||
echo Open the file and copy the 3 exe files from the "bin" folder into the Red-DiscordBot folder
|
|
||||||
PAUSE
|
|
||||||
GOTO end
|
|
||||||
|
|
||||||
:64bit
|
|
||||||
echo Downloading files... Do not close.
|
|
||||||
echo Downloading ffmpeg.exe (1/3)...
|
|
||||||
powershell -Command "(New-Object Net.WebClient).DownloadFile('https://github.com/Twentysix26/Red-DiscordBot/raw/master/ffmpeg.exe', 'ffmpeg.exe')"
|
|
||||||
echo Downloading ffplay.exe (2/3)...
|
|
||||||
powershell -Command "(New-Object Net.WebClient).DownloadFile('https://github.com/Twentysix26/Red-DiscordBot/raw/master/ffplay.exe', 'ffplay.exe')"
|
|
||||||
echo Downloading ffprobe.exe (3/3)...
|
|
||||||
powershell -Command "(New-Object Net.WebClient).DownloadFile('https://github.com/Twentysix26/Red-DiscordBot/raw/master/ffprobe.exe', 'ffprobe.exe')"
|
|
||||||
PAUSE
|
|
||||||
|
|
||||||
:end
|
|
||||||
536
launcher.py
Normal file
536
launcher.py
Normal file
@ -0,0 +1,536 @@
|
|||||||
|
from __future__ import print_function
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
try: # Older Pythons lack this
|
||||||
|
import urllib.request # We'll let them reach the Python
|
||||||
|
except ImportError: # check anyway
|
||||||
|
pass
|
||||||
|
import platform
|
||||||
|
import webbrowser
|
||||||
|
import hashlib
|
||||||
|
import argparse
|
||||||
|
import shutil
|
||||||
|
import stat
|
||||||
|
import time
|
||||||
|
try:
|
||||||
|
import pip
|
||||||
|
except ImportError:
|
||||||
|
pip = None
|
||||||
|
|
||||||
|
REQS_DIR = "lib"
|
||||||
|
sys.path.insert(0, REQS_DIR)
|
||||||
|
REQS_TXT = "requirements.txt"
|
||||||
|
REQS_NO_AUDIO_TXT = "requirements_no_audio.txt"
|
||||||
|
FFMPEG_BUILDS_URL = "https://ffmpeg.zeranoe.com/builds/"
|
||||||
|
|
||||||
|
INTRO = ("==========================\n"
|
||||||
|
"Red Discord Bot - Launcher\n"
|
||||||
|
"==========================\n")
|
||||||
|
|
||||||
|
IS_WINDOWS = os.name == "nt"
|
||||||
|
IS_MAC = sys.platform == "darwin"
|
||||||
|
IS_64BIT = platform.machine().endswith("64")
|
||||||
|
INTERACTIVE_MODE = not len(sys.argv) > 1 # CLI flags = non-interactive
|
||||||
|
PYTHON_OK = sys.version_info >= (3, 5)
|
||||||
|
|
||||||
|
FFMPEG_FILES = {
|
||||||
|
"ffmpeg.exe" : "e0d60f7c0d27ad9d7472ddf13e78dc89",
|
||||||
|
"ffplay.exe" : "d100abe8281cbcc3e6aebe550c675e09",
|
||||||
|
"ffprobe.exe" : "0e84b782c0346a98434ed476e937764f"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_cli_arguments():
|
||||||
|
parser = argparse.ArgumentParser(description="Red - Discord Bot's launcher")
|
||||||
|
parser.add_argument("--start", "-s",
|
||||||
|
help="Starts Red",
|
||||||
|
action="store_true")
|
||||||
|
parser.add_argument("--auto-restart",
|
||||||
|
help="Autorestarts Red in case of issues",
|
||||||
|
action="store_true")
|
||||||
|
parser.add_argument("--update-red",
|
||||||
|
help="Updates Red (git)",
|
||||||
|
action="store_true")
|
||||||
|
parser.add_argument("--update-reqs",
|
||||||
|
help="Updates requirements (w/ audio)",
|
||||||
|
action="store_true")
|
||||||
|
parser.add_argument("--update-reqs-no-audio",
|
||||||
|
help="Updates requirements (w/o audio)",
|
||||||
|
action="store_true")
|
||||||
|
parser.add_argument("--repair",
|
||||||
|
help="Issues a git reset --hard",
|
||||||
|
action="store_true")
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def install_reqs(audio):
|
||||||
|
interpreter = sys.executable
|
||||||
|
|
||||||
|
if interpreter is None:
|
||||||
|
print("Python interpreter not found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
txt = REQS_TXT if audio else REQS_NO_AUDIO_TXT
|
||||||
|
|
||||||
|
args = [
|
||||||
|
interpreter, "-m",
|
||||||
|
"pip", "install",
|
||||||
|
"--upgrade",
|
||||||
|
"--target", REQS_DIR,
|
||||||
|
"-r", txt
|
||||||
|
]
|
||||||
|
|
||||||
|
if IS_MAC: # --target is a problem on Homebrew. See PR #552
|
||||||
|
args.remove("--target")
|
||||||
|
args.remove(REQS_DIR)
|
||||||
|
|
||||||
|
code = subprocess.call(args)
|
||||||
|
|
||||||
|
if code == 0:
|
||||||
|
print("\nRequirements setup completed.")
|
||||||
|
else:
|
||||||
|
print("\nAn error occured and the requirements setup might "
|
||||||
|
"not be completed. Consult the docs.\n")
|
||||||
|
|
||||||
|
|
||||||
|
def update_pip():
|
||||||
|
interpreter = sys.executable
|
||||||
|
|
||||||
|
if interpreter is None:
|
||||||
|
print("Python interpreter not found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
args = [
|
||||||
|
interpreter, "-m",
|
||||||
|
"pip", "install",
|
||||||
|
"--upgrade", "pip"
|
||||||
|
]
|
||||||
|
|
||||||
|
code = subprocess.call(args)
|
||||||
|
|
||||||
|
if code == 0:
|
||||||
|
print("\nPip has been updated.")
|
||||||
|
else:
|
||||||
|
print("\nAn error occurred and pip might not have been updated.")
|
||||||
|
|
||||||
|
|
||||||
|
def update_red():
|
||||||
|
try:
|
||||||
|
code = subprocess.call(("git", "pull", "--ff-only"))
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("\nError: Git not found. It's either not installed or not in "
|
||||||
|
"the PATH environment variable like requested in the guide.")
|
||||||
|
return
|
||||||
|
if code == 0:
|
||||||
|
print("\nRed has been updated")
|
||||||
|
else:
|
||||||
|
print("\nRed could not update properly. If this is caused by edits "
|
||||||
|
"you have made to the code you can try the repair option from "
|
||||||
|
"the Maintenance submenu")
|
||||||
|
|
||||||
|
|
||||||
|
def reset_red(reqs=False, data=False, cogs=False, git_reset=False):
|
||||||
|
if reqs:
|
||||||
|
try:
|
||||||
|
shutil.rmtree(REQS_DIR, onerror=remove_readonly)
|
||||||
|
print("Installed local packages have been wiped.")
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
print("An error occured when trying to remove installed "
|
||||||
|
"requirements: {}".format(e))
|
||||||
|
if data:
|
||||||
|
try:
|
||||||
|
shutil.rmtree("data", onerror=remove_readonly)
|
||||||
|
print("'data' folder has been wiped.")
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
print("An error occured when trying to remove the 'data' folder: "
|
||||||
|
"{}".format(e))
|
||||||
|
|
||||||
|
if cogs:
|
||||||
|
try:
|
||||||
|
shutil.rmtree("cogs", onerror=remove_readonly)
|
||||||
|
print("'cogs' folder has been wiped.")
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
print("An error occured when trying to remove the 'cogs' folder: "
|
||||||
|
"{}".format(e))
|
||||||
|
|
||||||
|
if git_reset:
|
||||||
|
code = subprocess.call(("git", "reset", "--hard"))
|
||||||
|
if code == 0:
|
||||||
|
print("Red has been restored to the last local commit.")
|
||||||
|
else:
|
||||||
|
print("The repair has failed.")
|
||||||
|
|
||||||
|
|
||||||
|
def download_ffmpeg(bitness):
|
||||||
|
clear_screen()
|
||||||
|
repo = "https://github.com/Twentysix26/Red-DiscordBot/raw/master/"
|
||||||
|
verified = []
|
||||||
|
|
||||||
|
if bitness == "32bit":
|
||||||
|
print("Please download 'ffmpeg 32bit static' from the page that "
|
||||||
|
"is about to open.\nOnce done, open the 'bin' folder located "
|
||||||
|
"inside the zip.\nThere should be 3 files: ffmpeg.exe, "
|
||||||
|
"ffplay.exe, ffprobe.exe.\nPut all three of them into the "
|
||||||
|
"bot's main folder.")
|
||||||
|
time.sleep(4)
|
||||||
|
webbrowser.open(FFMPEG_BUILDS_URL)
|
||||||
|
return
|
||||||
|
|
||||||
|
for filename in FFMPEG_FILES:
|
||||||
|
if os.path.isfile(filename):
|
||||||
|
print("{} already present. Verifying integrity... "
|
||||||
|
"".format(filename), end="")
|
||||||
|
_hash = calculate_md5(filename)
|
||||||
|
if _hash == FFMPEG_FILES[filename]:
|
||||||
|
verified.append(filename)
|
||||||
|
print("Ok")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
print("Hash mismatch. Redownloading.")
|
||||||
|
print("Downloading {}... Please wait.".format(filename))
|
||||||
|
with urllib.request.urlopen(repo + filename) as data:
|
||||||
|
with open(filename, "wb") as f:
|
||||||
|
f.write(data.read())
|
||||||
|
print("Download completed.")
|
||||||
|
|
||||||
|
for filename, _hash in FFMPEG_FILES.items():
|
||||||
|
if filename in verified:
|
||||||
|
continue
|
||||||
|
print("Verifying {}... ".format(filename), end="")
|
||||||
|
if not calculate_md5(filename) != _hash:
|
||||||
|
print("Passed.")
|
||||||
|
else:
|
||||||
|
print("Hash mismatch. Please redownload.")
|
||||||
|
|
||||||
|
print("\nAll files have been downloaded.")
|
||||||
|
|
||||||
|
|
||||||
|
def verify_requirements():
|
||||||
|
try:
|
||||||
|
from discord.ext import commands
|
||||||
|
except ImportError:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def is_dpy_audio_installed():
|
||||||
|
"""Detects if the audio portion of discord.py is installed"""
|
||||||
|
if not verify_requirements:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
import nacl.secret
|
||||||
|
except ImportError:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def requirements_menu():
|
||||||
|
clear_screen()
|
||||||
|
while True:
|
||||||
|
print(INTRO)
|
||||||
|
print("Main requirements:\n")
|
||||||
|
print("1. Install basic + audio requirements (recommended)")
|
||||||
|
print("2. Install basic requirements")
|
||||||
|
if IS_WINDOWS:
|
||||||
|
print("\nffmpeg (audio requirement):")
|
||||||
|
print("3. Install ffmpeg 32bit")
|
||||||
|
if IS_64BIT:
|
||||||
|
print("4. Install ffmpeg 64bit (recommended on Windows 64bit)")
|
||||||
|
print("\n0. Go back")
|
||||||
|
choice = user_choice()
|
||||||
|
if choice == "1":
|
||||||
|
install_reqs(audio=True)
|
||||||
|
wait()
|
||||||
|
elif choice == "2":
|
||||||
|
install_reqs(audio=False)
|
||||||
|
wait()
|
||||||
|
elif choice == "3" and IS_WINDOWS:
|
||||||
|
download_ffmpeg(bitness="32bit")
|
||||||
|
wait()
|
||||||
|
elif choice == "4" and (IS_WINDOWS and IS_64BIT):
|
||||||
|
download_ffmpeg(bitness="64bit")
|
||||||
|
wait()
|
||||||
|
elif choice == "0":
|
||||||
|
break
|
||||||
|
clear_screen()
|
||||||
|
|
||||||
|
|
||||||
|
def update_menu():
|
||||||
|
clear_screen()
|
||||||
|
while True:
|
||||||
|
print(INTRO)
|
||||||
|
print("Update:\n")
|
||||||
|
print("Red:")
|
||||||
|
print("1. Update Red + requirements (recommended)")
|
||||||
|
print("2. Update Red")
|
||||||
|
print("3. Update requirements")
|
||||||
|
print("\nOthers:")
|
||||||
|
print("4. Update pip (might require admin privileges)")
|
||||||
|
print("\n0. Go back")
|
||||||
|
choice = user_choice()
|
||||||
|
if choice == "1":
|
||||||
|
update_red()
|
||||||
|
print("Updating requirements...")
|
||||||
|
audio = is_dpy_audio_installed()
|
||||||
|
if audio is not None:
|
||||||
|
install_reqs(audio=audio)
|
||||||
|
else:
|
||||||
|
print("The requirements haven't been installed yet.")
|
||||||
|
wait()
|
||||||
|
elif choice == "2":
|
||||||
|
update_red()
|
||||||
|
wait()
|
||||||
|
elif choice == "3":
|
||||||
|
audio = is_dpy_audio_installed()
|
||||||
|
if audio is not None:
|
||||||
|
install_reqs(audio=audio)
|
||||||
|
else:
|
||||||
|
print("The requirements haven't been installed yet.")
|
||||||
|
wait()
|
||||||
|
elif choice == "4":
|
||||||
|
update_pip()
|
||||||
|
wait()
|
||||||
|
elif choice == "0":
|
||||||
|
break
|
||||||
|
clear_screen()
|
||||||
|
|
||||||
|
|
||||||
|
def maintenance_menu():
|
||||||
|
clear_screen()
|
||||||
|
while True:
|
||||||
|
print(INTRO)
|
||||||
|
print("Maintenance:\n")
|
||||||
|
print("1. Repair Red (discards code changes, keeps data intact)")
|
||||||
|
print("2. Wipe 'data' folder (all settings, cogs' data...)")
|
||||||
|
print("3. Wipe 'lib' folder (all local requirements / local installed"
|
||||||
|
" python packages)")
|
||||||
|
print("4. Factory reset")
|
||||||
|
print("\n0. Go back")
|
||||||
|
choice = user_choice()
|
||||||
|
if choice == "1":
|
||||||
|
print("Any code modification you have made will be lost. Data/"
|
||||||
|
"non-default cogs will be left intact. Are you sure?")
|
||||||
|
if user_pick_yes_no():
|
||||||
|
reset_red(git_reset=True)
|
||||||
|
wait()
|
||||||
|
elif choice == "2":
|
||||||
|
print("Are you sure? This will wipe the 'data' folder, which "
|
||||||
|
"contains all your settings and cogs' data.\nThe 'cogs' "
|
||||||
|
"folder, however, will be left intact.")
|
||||||
|
if user_pick_yes_no():
|
||||||
|
reset_red(data=True)
|
||||||
|
wait()
|
||||||
|
elif choice == "3":
|
||||||
|
reset_red(reqs=True)
|
||||||
|
wait()
|
||||||
|
elif choice == "4":
|
||||||
|
print("Are you sure? This will wipe ALL your Red's installation "
|
||||||
|
"data.\nYou'll lose all your settings, cogs and any "
|
||||||
|
"modification you have made.\nThere is no going back.")
|
||||||
|
if user_pick_yes_no():
|
||||||
|
reset_red(reqs=True, data=True, cogs=True, git_reset=True)
|
||||||
|
wait()
|
||||||
|
elif choice == "0":
|
||||||
|
break
|
||||||
|
clear_screen()
|
||||||
|
|
||||||
|
|
||||||
|
def run_red(autorestart):
|
||||||
|
interpreter = sys.executable
|
||||||
|
|
||||||
|
if interpreter is None: # This should never happen
|
||||||
|
raise RuntimeError("Couldn't find Python's interpreter")
|
||||||
|
|
||||||
|
if not verify_requirements():
|
||||||
|
print("You don't have the requirements to start Red. "
|
||||||
|
"Install them from the launcher.")
|
||||||
|
if not INTERACTIVE_MODE:
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
cmd = (interpreter, "red.py")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
code = subprocess.call(cmd)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
code = 0
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if code == 0:
|
||||||
|
break
|
||||||
|
elif code == 26:
|
||||||
|
print("Restarting Red...")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if not autorestart:
|
||||||
|
break
|
||||||
|
|
||||||
|
print("Red has been terminated. Exit code: %d" % code)
|
||||||
|
|
||||||
|
if INTERACTIVE_MODE:
|
||||||
|
wait()
|
||||||
|
|
||||||
|
|
||||||
|
def clear_screen():
|
||||||
|
if IS_WINDOWS:
|
||||||
|
os.system("cls")
|
||||||
|
else:
|
||||||
|
os.system("clear")
|
||||||
|
|
||||||
|
|
||||||
|
def wait():
|
||||||
|
if INTERACTIVE_MODE:
|
||||||
|
input("Press enter to continue.")
|
||||||
|
|
||||||
|
|
||||||
|
def user_choice():
|
||||||
|
return input("> ").lower().strip()
|
||||||
|
|
||||||
|
|
||||||
|
def user_pick_yes_no():
|
||||||
|
choice = None
|
||||||
|
yes = ("yes", "y")
|
||||||
|
no = ("no", "n")
|
||||||
|
while choice not in yes and choice not in no:
|
||||||
|
choice = input("Yes/No > ").lower().strip()
|
||||||
|
return choice in yes
|
||||||
|
|
||||||
|
|
||||||
|
def remove_readonly(func, path, excinfo):
|
||||||
|
os.chmod(path, stat.S_IWRITE)
|
||||||
|
func(path)
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_md5(filename):
|
||||||
|
hash_md5 = hashlib.md5()
|
||||||
|
with open(filename, "rb") as f:
|
||||||
|
for chunk in iter(lambda: f.read(4096), b""):
|
||||||
|
hash_md5.update(chunk)
|
||||||
|
return hash_md5.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def create_fast_start_scripts():
|
||||||
|
"""Creates scripts for fast boot of Red without going
|
||||||
|
through the launcher"""
|
||||||
|
interpreter = sys.executable
|
||||||
|
if not interpreter:
|
||||||
|
return
|
||||||
|
|
||||||
|
call = "\"{}\" launcher.py".format(interpreter)
|
||||||
|
start_red = "{} --start".format(call)
|
||||||
|
start_red_autorestart = "{} --start --auto-restart".format(call)
|
||||||
|
modified = False
|
||||||
|
|
||||||
|
if IS_WINDOWS:
|
||||||
|
pause = "\npause"
|
||||||
|
ext = ".bat"
|
||||||
|
else:
|
||||||
|
pause = "\nread -rsp $'Press enter to continue...\n'"
|
||||||
|
if not IS_MAC:
|
||||||
|
ext = ".sh"
|
||||||
|
else:
|
||||||
|
ext = ".command"
|
||||||
|
|
||||||
|
start_red = start_red + pause
|
||||||
|
start_red_autorestart = start_red_autorestart + pause
|
||||||
|
|
||||||
|
files = {
|
||||||
|
"start_red" + ext : start_red,
|
||||||
|
"start_red_autorestart" + ext : start_red_autorestart
|
||||||
|
}
|
||||||
|
|
||||||
|
for filename, content in files.items():
|
||||||
|
if not os.path.isfile(filename):
|
||||||
|
print("Creating {}... (fast start scripts)".format(filename))
|
||||||
|
modified = True
|
||||||
|
with open(filename, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
if not IS_WINDOWS and modified: # Let's make them executable on Unix
|
||||||
|
for script in ("start_red.sh", "start_red_autorestart.sh"):
|
||||||
|
st = os.stat(script)
|
||||||
|
os.chmod(script, st.st_mode | stat.S_IEXEC)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if IS_WINDOWS:
|
||||||
|
os.system("TITLE Red Discord Bot - Launcher")
|
||||||
|
clear_screen()
|
||||||
|
|
||||||
|
try:
|
||||||
|
create_fast_start_scripts()
|
||||||
|
except Exception as e:
|
||||||
|
print("Failed making fast start scripts: {}\n".format(e))
|
||||||
|
|
||||||
|
while True:
|
||||||
|
print(INTRO)
|
||||||
|
|
||||||
|
if not os.path.isdir(".git"):
|
||||||
|
print("WARNING: It doesnt' look like Red has been "
|
||||||
|
"installed with git.\nThis means that you won't "
|
||||||
|
"be able to update and some features won't be working.\n"
|
||||||
|
"A reinstallation is recommended. Follow the guide "
|
||||||
|
"properly this time:\n"
|
||||||
|
"https://twentysix26.github.io/Red-Docs/\n")
|
||||||
|
|
||||||
|
print("1. Run Red /w autorestart in case of issues")
|
||||||
|
print("2. Run Red")
|
||||||
|
print("3. Update")
|
||||||
|
print("4. Install requirements")
|
||||||
|
print("5. Maintenance (repair, reset...)")
|
||||||
|
print("\n0. Quit")
|
||||||
|
choice = user_choice()
|
||||||
|
if choice == "1":
|
||||||
|
run_red(autorestart=True)
|
||||||
|
elif choice == "2":
|
||||||
|
run_red(autorestart=False)
|
||||||
|
elif choice == "3":
|
||||||
|
update_menu()
|
||||||
|
elif choice == "4":
|
||||||
|
requirements_menu()
|
||||||
|
elif choice == "5":
|
||||||
|
maintenance_menu()
|
||||||
|
elif choice == "0":
|
||||||
|
break
|
||||||
|
clear_screen()
|
||||||
|
|
||||||
|
args = parse_cli_arguments()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
abspath = os.path.abspath(__file__)
|
||||||
|
dirname = os.path.dirname(abspath)
|
||||||
|
# Sets current directory to the script's
|
||||||
|
os.chdir(dirname)
|
||||||
|
if not PYTHON_OK:
|
||||||
|
print("Red needs Python 3.5 or superior. Install the required "
|
||||||
|
"version.\nPress enter to continue.")
|
||||||
|
if INTERACTIVE_MODE:
|
||||||
|
wait()
|
||||||
|
exit(1)
|
||||||
|
if pip is None:
|
||||||
|
print("Red cannot work without the pip module. Please make sure to "
|
||||||
|
"install Python without unchecking any option during the setup")
|
||||||
|
wait()
|
||||||
|
exit(1)
|
||||||
|
if args.repair:
|
||||||
|
reset_red(git_reset=True)
|
||||||
|
if args.update_red:
|
||||||
|
update_red()
|
||||||
|
if args.update_reqs:
|
||||||
|
install_reqs(audio=True)
|
||||||
|
elif args.update_reqs_no_audio:
|
||||||
|
install_reqs(audio=False)
|
||||||
|
if INTERACTIVE_MODE:
|
||||||
|
main()
|
||||||
|
elif args.start:
|
||||||
|
print("Starting Red...")
|
||||||
|
run_red(autorestart=args.auto_restart)
|
||||||
57
red.py
57
red.py
@ -1,10 +1,12 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
sys.path.insert(0, "lib")
|
||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
import traceback
|
import traceback
|
||||||
import datetime
|
import datetime
|
||||||
|
import subprocess
|
||||||
|
|
||||||
try:
|
try:
|
||||||
assert sys.version_info >= (3, 5)
|
assert sys.version_info >= (3, 5)
|
||||||
@ -64,6 +66,7 @@ class Bot(commands.Bot):
|
|||||||
self._message_modifiers = []
|
self._message_modifiers = []
|
||||||
self.settings = Settings()
|
self.settings = Settings()
|
||||||
self._intro_displayed = False
|
self._intro_displayed = False
|
||||||
|
self._restart_requested = False
|
||||||
self.logger = set_logger(self)
|
self.logger = set_logger(self)
|
||||||
if 'self_bot' in kwargs:
|
if 'self_bot' in kwargs:
|
||||||
self.settings.self_bot = kwargs['self_bot']
|
self.settings.self_bot = kwargs['self_bot']
|
||||||
@ -91,6 +94,15 @@ class Bot(commands.Bot):
|
|||||||
|
|
||||||
return await super().send_message(*args, **kwargs)
|
return await super().send_message(*args, **kwargs)
|
||||||
|
|
||||||
|
async def shutdown(self, *, restart=False):
|
||||||
|
"""Gracefully quits Red with exit code 0
|
||||||
|
|
||||||
|
If restart is True, the exit code will be 26 instead
|
||||||
|
The launcher automatically restarts Red when that happens"""
|
||||||
|
if restart:
|
||||||
|
self._restart_requested = True
|
||||||
|
await self.logout()
|
||||||
|
|
||||||
def add_message_modifier(self, func):
|
def add_message_modifier(self, func):
|
||||||
"""
|
"""
|
||||||
Adds a message modifier to the bot
|
Adds a message modifier to the bot
|
||||||
@ -174,6 +186,40 @@ class Bot(commands.Bot):
|
|||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
async def pip_install(self, name, *, timeout=None):
|
||||||
|
"""
|
||||||
|
Installs a pip package in the local 'lib' folder in a thread safe
|
||||||
|
way. On Mac systems the 'lib' folder is not used.
|
||||||
|
Can specify the max seconds to wait for the task to complete
|
||||||
|
|
||||||
|
Returns a bool indicating if the installation was successful
|
||||||
|
"""
|
||||||
|
|
||||||
|
IS_MAC = sys.platform == "darwin"
|
||||||
|
interpreter = sys.executable
|
||||||
|
|
||||||
|
if interpreter is None:
|
||||||
|
raise RuntimeError("Couldn't find Python's interpreter")
|
||||||
|
|
||||||
|
args = [
|
||||||
|
interpreter, "-m",
|
||||||
|
"pip", "install",
|
||||||
|
"--upgrade",
|
||||||
|
"--target", "lib",
|
||||||
|
name
|
||||||
|
]
|
||||||
|
|
||||||
|
if IS_MAC: # --target is a problem on Homebrew. See PR #552
|
||||||
|
args.remove("--target")
|
||||||
|
args.remove("lib")
|
||||||
|
|
||||||
|
def install():
|
||||||
|
code = subprocess.call(args)
|
||||||
|
return not bool(code)
|
||||||
|
|
||||||
|
response = self.loop.run_in_executor(None, install)
|
||||||
|
return await asyncio.wait_for(response, timeout=timeout)
|
||||||
|
|
||||||
|
|
||||||
class Formatter(commands.HelpFormatter):
|
class Formatter(commands.HelpFormatter):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -282,13 +328,8 @@ def initialize(bot_class=Bot, formatter_class=Formatter):
|
|||||||
|
|
||||||
print("\nOfficial server: https://discord.me/Red-DiscordBot")
|
print("\nOfficial server: https://discord.me/Red-DiscordBot")
|
||||||
|
|
||||||
if os.name == "nt" and os.path.isfile("update.bat"):
|
print("Make sure to keep your bot updated. Select the 'Update' "
|
||||||
print("\nMake sure to keep your bot updated by running the file "
|
"option from the launcher.")
|
||||||
"update.bat")
|
|
||||||
else:
|
|
||||||
print("\nMake sure to keep your bot updated by using: git pull")
|
|
||||||
print("and: pip3 install -U git+https://github.com/Rapptz/"
|
|
||||||
"discord.py@master#egg=discord.py[voice]")
|
|
||||||
|
|
||||||
await bot.get_cog('Owner').disable_commands()
|
await bot.get_cog('Owner').disable_commands()
|
||||||
|
|
||||||
@ -580,3 +621,5 @@ if __name__ == '__main__':
|
|||||||
loop.close()
|
loop.close()
|
||||||
if error:
|
if error:
|
||||||
exit(1)
|
exit(1)
|
||||||
|
if bot._restart_requested:
|
||||||
|
exit(26)
|
||||||
|
|||||||
4
requirements_no_audio.txt
Normal file
4
requirements_no_audio.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
pip
|
||||||
|
git+git://github.com/Rapptz/discord.py.git
|
||||||
|
youtube_dl
|
||||||
|
imgurpython
|
||||||
@ -1,37 +0,0 @@
|
|||||||
@echo off
|
|
||||||
chcp 65001
|
|
||||||
echo.
|
|
||||||
pushd %~dp0
|
|
||||||
|
|
||||||
:loopstart
|
|
||||||
|
|
||||||
::Attempts to start py launcher without relying on PATH
|
|
||||||
%SYSTEMROOT%\py.exe --version > NUL 2>&1
|
|
||||||
IF %ERRORLEVEL% NEQ 0 GOTO attempt
|
|
||||||
%SYSTEMROOT%\py.exe -3 red.py
|
|
||||||
timeout 3
|
|
||||||
GOTO loopstart
|
|
||||||
|
|
||||||
::Attempts to start py launcher by relying on PATH
|
|
||||||
:attempt
|
|
||||||
py.exe --version > NUL 2>&1
|
|
||||||
IF %ERRORLEVEL% NEQ 0 GOTO lastattempt
|
|
||||||
py.exe -3 red.py
|
|
||||||
timeout 3
|
|
||||||
GOTO loopstart
|
|
||||||
|
|
||||||
::As a last resort, attempts to start whatever Python there is
|
|
||||||
:lastattempt
|
|
||||||
python.exe --version > NUL 2>&1
|
|
||||||
IF %ERRORLEVEL% NEQ 0 GOTO message
|
|
||||||
python.exe red.py
|
|
||||||
timeout 3
|
|
||||||
GOTO loopstart
|
|
||||||
|
|
||||||
:message
|
|
||||||
echo Couldn't find a valid Python ^>3.5 installation. Python needs to be installed and available in the PATH environment
|
|
||||||
echo variable.
|
|
||||||
echo https://twentysix26.github.io/Red-Docs/red_win_requirements/#software
|
|
||||||
PAUSE
|
|
||||||
|
|
||||||
:end
|
|
||||||
@ -6,7 +6,7 @@ pushd %~dp0
|
|||||||
::Attempts to start py launcher without relying on PATH
|
::Attempts to start py launcher without relying on PATH
|
||||||
%SYSTEMROOT%\py.exe --version > NUL 2>&1
|
%SYSTEMROOT%\py.exe --version > NUL 2>&1
|
||||||
IF %ERRORLEVEL% NEQ 0 GOTO attempt
|
IF %ERRORLEVEL% NEQ 0 GOTO attempt
|
||||||
%SYSTEMROOT%\py.exe -3 red.py
|
%SYSTEMROOT%\py.exe -3 launcher.py
|
||||||
PAUSE
|
PAUSE
|
||||||
GOTO end
|
GOTO end
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ GOTO end
|
|||||||
:attempt
|
:attempt
|
||||||
py.exe --version > NUL 2>&1
|
py.exe --version > NUL 2>&1
|
||||||
IF %ERRORLEVEL% NEQ 0 GOTO lastattempt
|
IF %ERRORLEVEL% NEQ 0 GOTO lastattempt
|
||||||
py.exe -3 red.py
|
py.exe -3 launcher.py
|
||||||
PAUSE
|
PAUSE
|
||||||
GOTO end
|
GOTO end
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ GOTO end
|
|||||||
:lastattempt
|
:lastattempt
|
||||||
python.exe --version > NUL 2>&1
|
python.exe --version > NUL 2>&1
|
||||||
IF %ERRORLEVEL% NEQ 0 GOTO message
|
IF %ERRORLEVEL% NEQ 0 GOTO message
|
||||||
python.exe red.py
|
python.exe launcher.py
|
||||||
PAUSE
|
PAUSE
|
||||||
GOTO end
|
GOTO end
|
||||||
|
|
||||||
58
update.bat
58
update.bat
@ -1,58 +0,0 @@
|
|||||||
@echo off
|
|
||||||
chcp 65001
|
|
||||||
echo.
|
|
||||||
pushd %~dp0
|
|
||||||
|
|
||||||
net session >nul 2>&1
|
|
||||||
if NOT %errorLevel% == 0 (
|
|
||||||
echo This script NEEDS to be run as administrator.
|
|
||||||
echo Right click on it ^-^> Run as administrator
|
|
||||||
echo.
|
|
||||||
PAUSE
|
|
||||||
GOTO end
|
|
||||||
)
|
|
||||||
|
|
||||||
::Checking git and updating
|
|
||||||
git.exe --version > NUL 2>&1
|
|
||||||
IF %ERRORLEVEL% NEQ 0 GOTO gitmessage
|
|
||||||
echo Updating Red...
|
|
||||||
git stash
|
|
||||||
git pull
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo Updating requirements...
|
|
||||||
::Attempts to start py launcher without relying on PATH
|
|
||||||
%SYSTEMROOT%\py.exe --version > NUL 2>&1
|
|
||||||
IF %ERRORLEVEL% NEQ 0 GOTO attempt
|
|
||||||
%SYSTEMROOT%\py.exe -3 -m pip install --upgrade -r requirements.txt
|
|
||||||
PAUSE
|
|
||||||
GOTO end
|
|
||||||
|
|
||||||
::Attempts to start py launcher by relying on PATH
|
|
||||||
:attempt
|
|
||||||
py.exe --version > NUL 2>&1
|
|
||||||
IF %ERRORLEVEL% NEQ 0 GOTO lastattempt
|
|
||||||
py.exe -3 -m pip install --upgrade -r requirements.txt
|
|
||||||
PAUSE
|
|
||||||
GOTO end
|
|
||||||
|
|
||||||
::As a last resort, attempts to start whatever Python there is
|
|
||||||
:lastattempt
|
|
||||||
python.exe --version > NUL 2>&1
|
|
||||||
IF %ERRORLEVEL% NEQ 0 GOTO pythonmessage
|
|
||||||
python.exe -m pip install --upgrade -r requirements.txt
|
|
||||||
PAUSE
|
|
||||||
GOTO end
|
|
||||||
|
|
||||||
:pythonmessage
|
|
||||||
echo Couldn't find a valid Python ^>3.5 installation. Python needs to be installed and available in the PATH environment variable.
|
|
||||||
echo https://twentysix26.github.io/Red-Docs/red_install_windows/#software
|
|
||||||
PAUSE
|
|
||||||
GOTO end
|
|
||||||
|
|
||||||
:gitmessage
|
|
||||||
echo Git is either not installed or not in the PATH environment variable. Install it again and add it to PATH like shown in the picture
|
|
||||||
echo https://twentysix26.github.io/Red-Docs/red_install_windows/#software
|
|
||||||
PAUSE
|
|
||||||
|
|
||||||
:end
|
|
||||||
Loading…
x
Reference in New Issue
Block a user