From b29b1bb338e8745c24d5348de8f65f4667b9e22f Mon Sep 17 00:00:00 2001 From: Dylan Jones Date: Sun, 29 Nov 2020 18:08:06 -0500 Subject: Begin installer code --- .gitignore | 2 + installer.py | 28 +++++++++++ modpackman.py | 105 ++++++++++++++++++++++++++++++++++++++-- util.py | 153 +++++++++++++++++----------------------------------------- version.txt | 8 +-- 5 files changed, 178 insertions(+), 118 deletions(-) create mode 100644 installer.py diff --git a/.gitignore b/.gitignore index e2fcdcb..77e38bf 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ geckodriver geckodriver.exe geckodriver.log __pycache__/ +*.swp +*.swo diff --git a/installer.py b/installer.py new file mode 100644 index 0000000..2df64a4 --- /dev/null +++ b/installer.py @@ -0,0 +1,28 @@ +import subprocess +import os +import requests +from util import config + + +def install_forge(java_path): + """ + :param java_path: path to a working Java executable + Downloads and runs the Forge installer specified in pack.ini. + """ + pass + + +def create_profile(): + """ + Automatically create a launcher profile for the modpack. + """ + pass + + + +def main(): + pass + + +if __name__ == '__main__': + main() diff --git a/modpackman.py b/modpackman.py index 55ebea6..77b0c2c 100755 --- a/modpackman.py +++ b/modpackman.py @@ -3,8 +3,106 @@ import argparse import os import sys import shutil +import requests +import pathlib +import hashlib import util +from util import config + +# Apply updates to the actual mod pack +def install(version_file, whitelist, mods_location): + pack_version = util.get_version_from_file(version_file) + print("Updating pack with version " + str(pack_version) + "...") + print() + + # create the mods folder if it doesn't already exist + pathlib.Path(mods_location).mkdir(parents=True, exist_ok=True) + + # (fname, checksum, url) + mods = util.read_file(version_file) + names = [mod[0] for mod in mods] + # whitelist client mods (e.g. optifine) + names += whitelist + + i = 0 + for mod in mods: + mod_path = os.path.join(mods_location, mod[0]) + i += 1 + if os.path.exists(mod_path) and os.path.isfile(mod_path) and \ + hashlib.sha1(open(mod_path, 'rb').read()).hexdigest() == mod[1]: + print("Skipping {mod[0]}, already up to date".format(mod=mod)) + else: + print('Installing {mod[0]} from {mod[2]}...'.format(mod=mod)) + print(' ({i} of {x})'.format(i=i,x=len(mods)), end='\r') + download_obj = requests.get(mod[2], stream=True) + with open(mod_path, "wb") as write_file: + shutil.copyfileobj(download_obj.raw, write_file) + print("Done!" + " " * 8) + + 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("Removing '{jar}'".format(jar=jar)) + + print() + print("Finished installing mods!") + + +# Using the latest urls, update downloads.txt to match and have the correct sha1 +def apply_updates(mods, version_file, game_version=(2, 0, 0)): + pack_version = util.get_version_from_file(version_file) + print("Populating version file...") + print("Getting new versions of all mods...") + mod_urls = util.find_updated_urls([x for x in mods.values()], game_version, threads=3) + print("Downloading and checksumming all mods...") + checksums = util.find_checksums(mod_urls) + + # Write information out to version.txt + with open(version_file, 'w') as f: + f.write('# Format: \n') + f.write("#VERSION " + str(pack_version + 1) + "\n") + for name, checksum, url in zip((k+'.jar' for k in mods.keys()), checksums, mod_urls): + f.write(f'{name} {checksum} {url}\n') + + print() + print("Done!") + print(f"Updates applied to {version_file}") + print("New pack version is " + str(pack_version + 1)) + 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( @@ -24,17 +122,16 @@ parser.add_argument('command', if __name__ == "__main__": args = parser.parse_args() - config = util.load_config() mods = config['mods'] pack = config['pack'] # run the command if args.command == "install": - util.install("version.txt", pack["whitelist"], os.path.join(pack['location'], 'mods')) + install("version.txt", config['pack']["whitelist"], os.path.join(config['pack']['location'], 'mods')) elif args.command == "apply_updates": - util.apply_updates(mods, "version.txt", pack["game_version"]) + apply_updates(config['mods'], "version.txt", config['pack']["game_version"]) elif args.command == "check_updates": - util.check_updates(mods, "version.txt", pack["game_version"]) + check_updates(config['mods'], "version.txt", config['pack']["game_version"]) else: print("Error: command \"" + args.command + "\" does not exist") parser.print_help() diff --git a/util.py b/util.py index 3e9e918..fba40fe 100644 --- a/util.py +++ b/util.py @@ -23,6 +23,9 @@ def load_config(): config = config_p._sections config["pack"]["sanitized_name"] = sanitize_text(config["pack"]["name"]) + if "location" not in config["pack"]: + config['pack']['location'] = os.path.join(find_minecraft_directory(), config['pack']['sanitized_name']) + if "whitelist" not in config["pack"]: config["pack"]["whitelist"] = [] else: @@ -30,19 +33,41 @@ def load_config(): config["pack"]["game_version"] = game_version_from_string(config["pack"]["game_version"]) - if "location" not in config["pack"]: - if sys.platform == "linux": - config["pack"]["location"] = os.path.join(os.path.expanduser('~'), ".minecraft", config['pack']['sanitized_name']) - elif sys.platform == "win32": - config["pack"]["location"] = os.path.join(os.environ["APPDATA"], ".minecraft", config['pack']['sanitized_name']) - elif sys.platform == "darwin": - config["pack"]["location"] = os.path.join(os.path.expanduser('~'), "Library", "Application Support", "minecraft", config['pack']['sanitized_name']) - else: - raise RuntimeError(f"Unsupported operating system `{sys.platform}`. Please define a location for the pack in your `local-config.ini` file") - # return the whole config file, pack configuration and modlist return config + +def find_minecraft_directory(): + """ + Find the location of the user's .minecraft folder based on + their operating system. + :returns: the absolute path to the .minecraft directory + """ + if sys.platform == "linux": + return os.path.join(os.path.expanduser('~'), ".minecraft") + elif sys.platform == "win32": + return os.path.join(os.environ["APPDATA"], ".minecraft") + elif sys.platform == "darwin": + return os.path.join(os.path.expanduser('~'), "Library", "Application Support", "minecraft") + else: + raise RuntimeError(f"Unsupported operating system `{sys.platform}`. Please define a location for the pack in your `local-config.ini` file") + + +def find_jre(): + """ + Find a usable install of Java, either from a user-installed JRE or + from the Minecraft Launcher's integrated JRE. + + :return: the absolute path of a working Java executable + """ + if shutil.which("java") is not None: + return shutil.which("java") + if sys.platform == 'win32': # We can try and use the Minecraft Launcher's integrated JRE on Windows + if os.path.exists("C:\\Program Files (x86)\\Minecraft Launcher\\runtime\\jre-x64\\java.exe"): + return "C:\\Program Files (x86)\\Minecraft Launcher\\runtime\\jre-x64\\java.exe" + raise RuntimeError("Unable to detect an installed JRE. Please install Java in order to use modpackman.") + + # take a string and only keep filename-friendly parts def sanitize_text(text): sanitized = "" @@ -87,100 +112,6 @@ def game_version_from_string(string): return (2, 0, 0) -# Apply updates to the actual mod pack -def install(version_file, whitelist, mods_location): - pack_version = get_version_from_file(version_file) - print("Updating pack with version " + str(pack_version) + "...") - print() - - # create the mods folder if it doesn't already exist - pathlib.Path(mods_location).mkdir(parents=True, exist_ok=True) - - # (fname, checksum, url) - mods = read_file(version_file) - names = [mod[0] for mod in mods] - # whitelist client mods (e.g. optifine) - names += whitelist - - i = 0 - for mod in mods: - mod_path = os.path.join(mods_location, mod[0]) - i += 1 - if os.path.exists(mod_path) and os.path.isfile(mod_path) and \ - hashlib.sha1(open(mod_path, 'rb').read()).hexdigest() == mod[1]: - print("Skipping {mod[0]}, already up to date".format(mod=mod)) - else: - print('Installing {mod[0]} from {mod[2]}...'.format(mod=mod)) - print(' ({i} of {x})'.format(i=i,x=len(mods)), end='\r') - download_obj = requests.get(mod[2], stream=True) - with open(mod_path, "wb") as write_file: - shutil.copyfileobj(download_obj.raw, write_file) - print("Done!" + " " * 8) - - 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("Removing '{jar}'".format(jar=jar)) - - print() - print("Finished installing mods!") - - -# Using the latest urls, update downloads.txt to match and have the correct sha1 -def apply_updates(mods, version_file, game_version=(2, 0, 0)): - pack_version = get_version_from_file(version_file) - print("Populating version file...") - print("Getting new versions of all mods...") - mod_urls = find_updated_urls([x for x in mods.values()], game_version, threads=3) - print("Downloading and checksumming all mods...") - checksums = find_checksums(mod_urls) - - # Write information out to version.txt - with open(version_file, 'w') as f: - f.write('# Format: \n') - f.write("#VERSION " + str(pack_version + 1) + "\n") - for name, checksum, url in zip((k+'.jar' for k in mods.keys()), checksums, mod_urls): - f.write(f'{name} {checksum} {url}\n') - - print() - print("Done!") - print(f"Updates applied to {version_file}") - print("New pack version is " + str(pack_version + 1)) - 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 = 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 = read_file(version_file) - old_urls = [mod[2] for mod in old] - num_updates = 0 - - print("Checking updates...") - ffx = 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 = 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.") - def threaded_find_url(homepage_url, game_version): """ @@ -292,9 +223,7 @@ def find_cdn(ffx, url, version): return f'https://media.forgecdn.net/files/{int(best_row.cdn_id[:4])}/{int(best_row.cdn_id[4:])}/{filename}' except: - print(url) - open('temp.txt', 'a').write(url) - import traceback; traceback.print_exc() + # import traceback; traceback.print_exc() return None @@ -313,7 +242,11 @@ def firefox(): options.add_argument('-headless') options.add_argument('--window-size 1920,1080') #for ~~cursed~~ windows people, put geckodriver in this folder - if(os.path.exists("./geckodriver")): - return Firefox(executable_path='./geckodriver', options=options) - return Firefox(options=options) + if(os.path.exists("./geckodriver.exe")): + return Firefox(executable_path='./geckodriver') + return Firefox() + +# Configuration is automatically loaded from pack.ini and local-config.ini, +# and made accessible here as a global +config = load_config() diff --git a/version.txt b/version.txt index 710244e..c874116 100644 --- a/version.txt +++ b/version.txt @@ -1,5 +1,5 @@ # Format: -#VERSION 35 +#VERSION 36 building-gadgets.jar 13e89349f2929c205c6c80615c6e927f70c9b1e0 https://media.forgecdn.net/files/3097/299/buildinggadgets-3.7.2.jar item-collectors.jar a4b3eeb92dd79736ce622e92f49bd5ff2b0b84f0 https://media.forgecdn.net/files/3101/223/itemcollectors-1.0.6-mc1.16.4.jar natures-compass.jar 30ae4fe62fbfdff8d81f84a821588ba882851a04 https://media.forgecdn.net/files/3123/531/NaturesCompass-1.16.4-1.8.5.jar @@ -27,7 +27,7 @@ immersive-engineering.jar a1263bd875531ee5bf70937fa207f4509efb1cfe https://media botania.jar c890bd2ec0d13ead2627b0bf0c7599de6b2f6194 https://media.forgecdn.net/files/3086/502/Botania-1.16.3-409.jar jei.jar 2bbcffbbe7ac7c93e75783637a0a78fddcb4109d https://media.forgecdn.net/files/3109/181/jei-1.16.4-7.6.0.58.jar tetra.jar 6e097ef3053cc56b95973607db632d08ebf6c554 https://media.forgecdn.net/files/3104/240/tetra-1.16.4-3.3.1.jar -immersive-portals.jar 3fc06df79a96fc9e695c3f1091772e8cd2b175cd https://github-production-release-asset-2e65be.s3.amazonaws.com/316149911/5d2ca300-2f8e-11eb-8586-1e03f0825404?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20201127%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20201127T234755Z&X-Amz-Expires=300&X-Amz-Signature=2aa1740e6eb705062090827613c00ac60066b159a2530448633dddaf86d6093d&X-Amz-SignedHeaders=host&actor_id=0&key_id=0&repo_id=316149911&response-content-disposition=attachment%3B%20filename%3Dimmersive-portals-0.9.jar&response-content-type=application%2Foctet-stream +immersive-portals.jar 3fc06df79a96fc9e695c3f1091772e8cd2b175cd https://github-production-release-asset-2e65be.s3.amazonaws.com/316149911/5d2ca300-2f8e-11eb-8586-1e03f0825404?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20201129%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20201129T065251Z&X-Amz-Expires=300&X-Amz-Signature=181cc92add82b065cda2a9988f1a4d277f66e6a1e9554ed64187006e371e4ba6&X-Amz-SignedHeaders=host&actor_id=0&key_id=0&repo_id=316149911&response-content-disposition=attachment%3B%20filename%3Dimmersive-portals-0.9.jar&response-content-type=application%2Foctet-stream inventory-sorter.jar fb538c5a8eb1e8a7a2bc35f3344b816634d53e4e https://media.forgecdn.net/files/3077/903/inventorysorter-1.16.1-18.1.0.jar creeper-confetti.jar c1d06621ba453680fe31dd30fe11f6ea9bcc8e93 https://media.forgecdn.net/files/3063/500/creeperconfetti-3.4.jar hwyla.jar 79123ef4c447affe57e77ee241d78c494bd8948d https://media.forgecdn.net/files/3033/593/Hwyla-forge-1.10.11-B78_1.16.2.jar @@ -59,10 +59,10 @@ recipe-buffers.jar b6a5af3c63651ae8f5434cec4fa753a5bfc5fdbb https://media.forgec harvest.jar f9618079d9456c23133ab89909fdd1c9fbae56b8 https://media.forgecdn.net/files/3087/381/harvest-1.16.3-1.0.3.jar light-overlay.jar a3caff7768581d5d97b5433dd834dfa6d676aea9 https://media.forgecdn.net/files/3091/376/light-overlay-5.5.4.jar morpheus.jar da30b18acd23c270a7da07ed8feb693291efcf90 https://media.forgecdn.net/files/3114/135/Morpheus-1.16.4-4.2.68.jar -extreme-sound-muffler.jar 8144221087d309afcf68926849d4095becf9e5ab https://media.forgecdn.net/files/3117/97/extremeSoundMuffler-3.0_Forge-1.16.4.jar +extreme-sound-muffler.jar 056a06346f1129b5dcdafe335a508c0e8254a7de https://media.forgecdn.net/files/3125/582/extremeSoundMuffler-3.1_Forge-1.16.4.jar fast-workbench.jar c7f0296b5c569f1fe11aa6368b87bebfa01c9ddc https://media.forgecdn.net/files/3112/661/FastWorkbench-1.16.3-4.4.1.jar findme.jar 7f18ff3af51b46c388d780c0b0afb9b400cc7962 https://media.forgecdn.net/files/3073/336/findme-1.16.3-2.1.0.0.jar -wawla.jar d9a4dbdfe97fa58e591aa0c4c714c13abb93c318 https://media.forgecdn.net/files/3113/92/WAWLA-1.16.4-7.0.1.jar +wawla.jar 9aeea4cc8ae5bfa89701435b7c71bffdc43cc49d https://media.forgecdn.net/files/3124/964/WAWLA-1.16.4-7.0.2.jar iron-chests.jar 3e7a9131d1ea836527d5cf7a1761d8b4dc7026a3 https://media.forgecdn.net/files/3105/315/ironchest-1.16.4-11.2.10.jar reauth.jar f5bdf682d64bcde250c8a2a9c1261e285540ac9b https://media.forgecdn.net/files/3105/779/ReAuth-1.16-Forge-3.9.3.jar yungs-caves.jar 6b4603a8fc2cb2aa13e8cf745865b25e10e25bde https://media.forgecdn.net/files/3110/318/BetterCaves-1.16.3-1.0.5.jar -- cgit v1.2.3