#!/usr/bin/env python3 import os import sys if hasattr(sys, '_MEIPASS'): # we're running in a bundle, go to where we have our bundled assets os.chdir(sys._MEIPASS) import argparse import shutil import requests import pathlib import hashlib from configparser import RawConfigParser import util from util import config # Apply updates to the actual mod pack def install(): mods_location = os.path.join(config["pack"]["location"], "mods") whitelist = config["pack"]["whitelist"] # Actual download links are stored in pack-lock.ini, so we need to read it in here pack_lock = RawConfigParser() pack_lock.read('pack-lock.ini') print(f"Updating pack with version {pack_lock['global']['pack_version']}...") # create the mods folder if it doesn't already exist pathlib.Path(mods_location).mkdir(parents=True, exist_ok=True) names = set(f'{mod}.jar' for mod in pack_lock['mod_versions'].keys()) # whitelist client mods (e.g. optifine) names.update(whitelist) i = 0 for entry in pack_lock['mod_versions'].items(): name = f'{entry[0]}.jar' checksum, url = entry[1].split(',') mod_path = os.path.join(mods_location, name) i += 1 if os.path.exists(mod_path) and os.path.isfile(mod_path) and \ hashlib.sha1(open(mod_path, 'rb').read()).hexdigest() == checksum: print(f"Skipping {name}, already up to date") else: print(f'Installing {name} from {url}...') print(f' ({i} of {len(pack_lock["mod_versions"])})', end='\r') download_obj = requests.get(url, stream=True) with open(mod_path, "wb") as write_file: shutil.copyfileobj(download_obj.raw, write_file) print("Done!" + " " * 8) # Remove any old mods that might be stuck in the mods folder print() print("Removing old mods...") for jar in os.listdir(mods_location): if jar not in names and os.path.splitext(jar)[1] == ".jar": os.remove(os.path.join(mods_location, jar)) print(f"Removing '{jar}'") print('\nInstalling configs...') # For config files, we don't need to remove any extras, as they # will simply be ignored by Forge if not os.path.exists('config'): raise RuntimeError("Error: config folder must exist!") mc_config_folder = os.path.join(config['pack']['location'], 'config') if not os.path.exists(mc_config_folder): os.mkdir(mc_config_folder) for cfg in os.listdir('config'): shutil.copyfile(os.path.join('config', cfg), os.path.join(mc_config_folder, cfg)) print() print("Finished installing mods!") def apply_updates(): """ Using the URLs defined in pack.ini, update all mods to the latest compatible version and write them out to pack-lock.ini """ print("Populating version file...") print("Getting new versions of all mods...") mod_urls = util.find_updated_urls([x for x in config['mods'].values()], config['pack']['game_version'], threads=16) print("Downloading and checksumming all mods...") checksums = util.find_checksums(mod_urls) # Write the updated mods list out to pack-lock.ini pack_lock = RawConfigParser() pack_lock.read('pack-lock.ini') pack_lock['global']['pack_version'] = str(int(pack_lock['global']['pack_version']) + 1) # This is only needed for the self-update feature pack_lock['global']['config_files'] = ','.join(os.listdir('config')) pack_lock['mods'] = {name: f'{checksum},{url}' for name, checksum, url in zip(config['mods'].keys(), checksums, mod_urls)} with open('pack-lock.ini', 'w') as f: pack_lock.write(f) print() print("Done!") print("Updates applied to pack-lock.ini") print(f"New pack version is {pack_lock['global']['pack_version']}") print("[!] No mods were installed. To update your mods folder, run 'update.py install'") # Find if any updates are available def check_updates(mods, version_file, version=(2, 0, 0)): pack_version = util.get_version_from_file(version_file) print("Checking for updates to version " + str(pack_version) + "...") latest = [(k, mods[k]) for k in mods.keys()] old = util.read_file(version_file) old_urls = [mod[2] for mod in old] num_updates = 0 print("Checking updates...") ffx = util.firefox() for mod in latest: print("Checking for updates to {mod[0]}...".format(mod=mod), end="") sys.stdout.flush() # takes care of line-buffered terminals if 'curseforge' in mod[1]: url = util.find_cdn(ffx, mod[1], version) else: url = requests.get(mod[1]).url if url in old_urls: print(" No updates") else: print(" Found update: " + url.split('/')[-1]) num_updates += 1 ffx.close() print("Finished checking for updates. {num} mods can be updated".format(num=num_updates)) if num_updates >= 0: print("Run 'python update.py apply_updates' to create a new version with these updates applied.") parser = argparse.ArgumentParser( description="A Simple Git-Based Modpack Manager", formatter_class=argparse.RawDescriptionHelpFormatter, epilog='''\ Available commands: install : Downloads mods listed in version.txt and populates the mods folder specified in pack-location.txt apply_updates : Using the urls in mods.txt, repopulates version.txt to reflect the most recent mod versions check_updates : Compares version.txt and mods.txt to see if any mods can be updated ''') parser.add_argument('command', nargs='?', default='install', help="The action to perform (default: install)") if __name__ == "__main__": args = parser.parse_args() mods = config['mods'] pack = config['pack'] # run the command if args.command == "install": install() elif args.command == "apply_updates": apply_updates() elif args.command == "check_updates": check_updates(config['mods'], "version.txt", config['pack']["game_version"]) else: print("Error: command \"" + args.command + "\" does not exist") parser.print_help() sys.exit(1)