import os import shutil import sys from pathlib import Path from src.version import __version__ import argparse import getpass import random from cryptography.fernet import Fernet import string def _resolve_botty_env(conda_path): """Find the botty Python environment directory. Tries (in order): 1. Explicit conda path (conda_path/envs/botty) 2. Current sys.prefix if it looks like a conda env 3. Fallback to sys.prefix (pip/virtualenv installs) """ # 1. Explicit conda path botty_env = os.path.join(conda_path, "envs", "botty") if os.path.isdir(botty_env): return botty_env # 2. Current prefix is a conda env if os.path.isfile(os.path.join(sys.prefix, "conda-meta", "history")) or \ os.path.isdir(os.path.join(sys.prefix, "Library")): return sys.prefix # 3. Plain pip / virtualenv — sys.prefix is the site return sys.prefix parser = argparse.ArgumentParser(description="Build Botty") parser.add_argument( "-v" , "--version", type=str, help="New release version e.g. 0.4.2", default="" ) parser.add_argument( "-c", "--conda_path", type=str, help="Path to local conda e.g. C:\\Users\\USER\\miniconda3", default=f"C:\\Users\\{getpass.getuser()}\\miniconda3") parser.add_argument( "-r", "--random_name", action='store_true', help="Will generate a random name for the botty exe") parser.add_argument( "-k", "--use_key", action='store_true', help="Will build with encryption key") args = parser.parse_args() # clean up def clean_up(): # pyinstaller if os.path.exists("build"): shutil.rmtree("build") if os.path.exists("main.spec"): os.remove("main.spec") if os.path.exists("health_manager.spec"): os.remove("health_manager.spec") if os.path.exists("shopper.spec"): os.remove("shopper.spec") if __name__ == "__main__": new_version_code = None if args.version != "": print(f"Releasing new version: {args.version}") os.system(f"git checkout -b new-release-v{args.version}") botty_dir = f"botty_v{args.version}" version_code = "" with open('src/version.py', 'r') as f: version_code = f.read() version_code = version_code.split("=") new_version_code = f"{version_code[0]}= '{args.version}'" with open('src/version.py', 'w') as f: f.write(new_version_code) else: botty_dir = f"botty_v{__version__}" print(f"Building version: {__version__}") clean_up() if os.path.exists(botty_dir): for path in Path(botty_dir).glob("**/*"): if path.is_file(): os.remove(path) elif path.is_dir(): shutil.rmtree(path) shutil.rmtree(botty_dir) botty_env = _resolve_botty_env(args.conda_path) pyinstaller_exe = os.path.join(botty_env, "Scripts", "pyinstaller.exe") if not os.path.isfile(pyinstaller_exe): raise RuntimeError(f"PyInstaller not found at {pyinstaller_exe}. " f"Install with: pip install pyinstaller") # DLL dirs for PyInstaller to resolve native dependencies. # Conda: Library\bin, Library\lib, DLLs # pip/virtualenv: just the system DLLs under sys.prefix dll_dirs = [] for d in ["Library/bin", "Library/lib", "DLLs"]: p = os.path.join(botty_env, d) if os.path.isdir(p): dll_dirs.append(p) if dll_dirs: os.environ["PATH"] = os.pathsep.join(dll_dirs) + os.pathsep + os.environ.get("PATH", "") for exe in ["main.py", "shopper.py"]: key_cmd = " " if args.use_key: key = Fernet.generate_key().decode("utf-8") key_cmd = " --key " + key installer_cmd = f'{pyinstaller_exe} --onefile --noconsole --distpath {botty_dir}{key_cmd} --exclude-module graphviz --exclude-module keyboard --exclude-module mouse --exclude-module pyclick --exclude-module mouseinfo --paths .\\src --paths "{botty_env}\\Lib\\site-packages" src\\{exe}' ret = os.system(installer_cmd) if ret != 0: raise RuntimeError(f"PyInstaller failed for {exe} (exit {ret})") os.makedirs(f"{botty_dir}/config", exist_ok=True) with open(f"{botty_dir}/config/custom.ini", "w") as f: f.write("; Add parameters you want to overwrite from param.ini here") shutil.copy("config/game.ini", f"{botty_dir}/config/") shutil.copy("config/params.ini", f"{botty_dir}/config/") shutil.copy("config/shop.ini", f"{botty_dir}/config/") shutil.copy("config/default.bnip", f"{botty_dir}/config/") os.makedirs(f"{botty_dir}/config/bnip", exist_ok=True) shutil.copy("README.md", f"{botty_dir}/") shutil.copytree("assets", f"{botty_dir}/assets") shutil.copytree("src", f"{botty_dir}/src") shutil.copy("environment.yml", f"{botty_dir}/") shutil.copy("install.bat", f"{botty_dir}/") shutil.copy("find_python.bat", f"{botty_dir}/") shutil.copy("run_botty.bat", f"{botty_dir}/") shutil.copy("run.bat", f"{botty_dir}/") if os.path.exists("dependencies"): shutil.copytree("dependencies", f"{botty_dir}/dependencies") # Bundle a portable Tesseract so the standalone exe is click-and-run with # working OCR and no separate install. ocr.py prefers /tesseract/ # tesseract.exe. Source: TESSERACT_DIR env or the default UB Mannheim path. # Skipped (with a warning) if not present — the bot still works once the # user runs install.bat, which sets OCR up the conda way. tesseract_src = os.environ.get("TESSERACT_DIR", r"C:\Program Files\Tesseract-OCR") tess_exe = os.path.join(tesseract_src, "tesseract.exe") if os.path.isfile(tess_exe): print(f"Bundling Tesseract from {tesseract_src}") # Copy the exe + DLLs; skip their tessdata (we ship our own trained # models in assets/tessdata and pass --tessdata-dir to point at them). os.makedirs(f"{botty_dir}/tesseract", exist_ok=True) for entry in os.listdir(tesseract_src): src = os.path.join(tesseract_src, entry) if os.path.isfile(src) and entry.lower().endswith((".exe", ".dll")): shutil.copy(src, f"{botty_dir}/tesseract/") else: print(f"WARNING: Tesseract not found at {tesseract_src} — release will " f"rely on install.bat for OCR setup. Set TESSERACT_DIR to bundle it.") clean_up() if args.random_name: print("Generate random names") new_name = ''.join(random.choices(string.ascii_letters, k=random.randint(6, 14))) os.rename(f'{botty_dir}/main.exe', f'{botty_dir}/{new_name}.exe') # Rename main.exe to avoid Warden flagging the obvious name # In CI/production builds (env BOTTY_NO_RENAME=1) keep main.exe as-is if not args.random_name and not os.environ.get("BOTTY_NO_RENAME"): new_name = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8)) os.rename(f'{botty_dir}/main.exe', f'{botty_dir}/{new_name}.exe') print(f"Renamed main.exe -> {new_name}.exe") if new_version_code is not None: os.system(f'git add .') os.system(f'git commit -m "Bump version to v{args.version}"')