cutthecord/resources/scripts/patchport.py
46620 1863fb57ea 95.6 - Alpha (95206)
Also fixed patchport again
I'm also running out of steam for this project and don't really wanna continue it much longer.
If official CTC gets slashcommands, I'll update this, but don't expect many updates for the next few months.
2021-10-02 15:40:24 -04:00

325 lines
14 KiB
Python

#!/bin/env python3
import re
import sys
import os
import json
import subprocess
import datetime
import shutil
# Example invocation:
# python3 patchport.py /home/ave/apks/com.discord-900/
with open("/opt/cutthecord/cutthecord/resources/patchport-state.json", "r") as f:
jin = json.load(f)
from_versioncode = jin["versioncode"]
from_versionname = jin["versionname"]
apk_folder = sys.argv[1]
cutthecord_folder = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
debug = False
tmp_folder = "/tmp/patchport"
if debug:
print(f"ctc folder: {cutthecord_folder}")
def modify_patch(patch_name, patch_path):
with open(patch_path) as f:
patch_content = f.read()
if patch_name in ["branding", "customversion"]:
patch_content = patch_content.replace(from_versioncode, to_versioncode)
patch_content = patch_content.replace(from_versionname, to_versionname)
return patch_content
def apply_patch(patch_contents):
subprocess.run("patch -p1 --no-backup-if-mismatch --force",
shell=True, input=patch_contents, text=True,
cwd=apk_folder, capture_output=True)
def fix_offset(patch_contents):
# OH GOD OH FUCK
shutil.rmtree(tmp_folder, ignore_errors=True)
os.makedirs(tmp_folder)
patch_lines = patch_contents.splitlines()
for line in patch_lines:
if "diff -crB" in line:
patch_target = line.split(" ")[2].replace("from/", "")
if not os.path.exists(os.path.join(apk_folder, patch_target)):
return False
os.makedirs(os.path.dirname(os.path.join(tmp_folder, patch_target)), exist_ok=True)
shutil.copy(os.path.join(apk_folder, patch_target), os.path.join(tmp_folder, patch_target))
# shutil.copytree(apk_folder, tmp_folder)
subprocess.run("patch -p1 --no-backup-if-mismatch --force",
shell=True, input=patch_contents, text=True,
cwd=tmp_folder, capture_output=True)
out = subprocess.run(f"diff -crB {apk_folder} {tmp_folder}",
shell=True, input=patch_contents, text=True,
cwd=tmp_folder, capture_output=True)
shutil.rmtree(tmp_folder, ignore_errors=True)
actual_difflines = []
for line in out.stdout.splitlines():
if line[0:4] != "Only":
actual_difflines.append(line)
patch_out = ("\n".join(actual_difflines)+"\n").replace(apk_folder, "from").replace(tmp_folder, "to")
return patch_out
# TODO replace with xml-patch version
def make_necessary(version_name, version_code):
# OH GOD OH FUCK
shutil.rmtree(tmp_folder, ignore_errors=True)
os.makedirs(tmp_folder)
shutil.copy(os.path.join(apk_folder, "AndroidManifest.xml"), os.path.join(tmp_folder, "AndroidManifest.xml"))
# Set version code and name
# Due to https://github.com/iBotPeaches/Apktool/issues/2046
# Code based on https://stackoverflow.com/a/4128192/3286892
with open(os.path.join(tmp_folder, "AndroidManifest.xml")) as fin:
filec = fin.read()
incorrect_versioncode = re_versioncode_xml.findall(filec)[0]
filec = filec.replace(incorrect_versioncode, f'platformBuildVersionCode="{to_versioncode}"')
incorrect_versionname = re_versionname_xml.findall(filec)[0]
filec = filec.replace(incorrect_versionname, f'platformBuildVersionName="{to_versionname}"')
with open(os.path.join(tmp_folder, "AndroidManifest.xml"), "w") as fout:
fout.write(filec)
out = subprocess.run(f"diff -crB {apk_folder} {tmp_folder}",
shell=True, text=True,
cwd=tmp_folder, capture_output=True)
shutil.rmtree(tmp_folder, ignore_errors=True)
actual_difflines = []
for line in out.stdout.splitlines():
if line[0:4] != "Only":
actual_difflines.append(line)
patch_out = ("\n".join(actual_difflines)+"\n").replace(apk_folder, "from").replace(tmp_folder, "to")
return patch_out
re_versioncode_xml = re.compile(r'(platformBuildVersionCode="[0-9]+")')
re_versionname_xml = re.compile(r'(platformBuildVersionName="[0-9a-z.]+")')
re_versioncode_yml = re.compile(r'versionCode: \'([0-9]+)\'')
re_versionname_yml = re.compile(r'versionName: \'?(.+?)\'?$')
re_releasedate = re.compile(r'released on ([0-9]{4}-[0-9]{2}-[0-9]{2})')
# Get version code and name
with open(os.path.join(apk_folder, "apktool.yml")) as f:
file_contents = f.read()
to_versioncode = re_versioncode_yml.findall(file_contents)[0]
to_versionname = re_versionname_yml.findall(file_contents)[0]
failures = []
for patch in os.listdir(os.path.join(cutthecord_folder, "resources/xmlpatches")):
if debug:
print(f"going over xml patch: {patch}")
# Ignore non-dirs
if not os.path.isdir(os.path.join(cutthecord_folder, "resources/xmlpatches", patch)):
if debug:
print(f"patch is not a folder, skipping: {patch}")
continue
pre_in_path = os.path.join(cutthecord_folder, "resources/xmlpatches", patch, f"{from_versioncode}-pre.sh")
post_in_path = os.path.join(cutthecord_folder, "resources/xmlpatches", patch, f"{from_versioncode}-post.sh")
pre_out_path = os.path.join(cutthecord_folder, "resources/xmlpatches", patch, f"{to_versioncode}-pre.sh")
post_out_path = os.path.join(cutthecord_folder, "resources/xmlpatches", patch, f"{to_versioncode}-post.sh")
patch_path = os.path.join(cutthecord_folder, "resources/xmlpatches", patch, f"{from_versioncode}.xml")
out_path = os.path.join(cutthecord_folder, "resources/xmlpatches", patch, f"{to_versioncode}.xml")
tmp_out_path = os.path.join("/tmp/", patch, f"{to_versioncode}.xml")
readme_path = os.path.join(cutthecord_folder, "resources/xmlpatches", patch, "README.md")
os.mkdir(os.path.dirname(tmp_out_path))
# Handle copying of versioned scripts, untested and dirty!
script_path = os.path.join(cutthecord_folder, "resources/xmlpatches", patch, f"{from_versioncode}.sh")
if os.path.exists(script_path):
script_out_path = os.path.join(cutthecord_folder, "resources/xmlpatches", patch, f"{to_versioncode}.sh")
with open(script_path) as f:
with open(script_out_path, "w") as f2:
f2.write(f.read())
# Check if patch exists for from_version, if it doesn't, warn user
if not os.path.isfile(patch_path) and patch not in ["necessary"]:
# Don't warn on instructional patches
if patch not in ["customfont", "customring", "bettertm", "bettertmlight", "blobs"]:
print(f"SKIPPED: No {from_versionname} version found for {patch}.")
continue
# Check if pre-script exists, if it does copy it
if os.path.isfile(pre_in_path):
shutil.copyfile(pre_in_path, pre_out_path)
print(f"PRE COPIED: {patch}'s pre script was copied.")
# Check if post-script exists, if it does copy it
if os.path.isfile(post_in_path):
shutil.copyfile(post_in_path, post_out_path)
print(f"POST COPIED: {patch}'s post script was copied.")
# Create necessary instead of porting it.
if patch == "necessary":
patch_contents = make_necessary(to_versioncode, to_versionname)
else:
# Get a modified version of the patch
patch_contents = modify_patch(patch, patch_path)
# Pass the new patch to patch command and get it to attempt to patch
with open(tmp_out_path, "w") as f:
f.write(patch_contents)
out = subprocess.run("java -cp /opt/ctc/tools/xml-patch.jar com.github.dnault.xmlpatch.BatchPatcher --patch {tmp_out_path} --srcdir {apk_folder} --destdir /tmp/", shell=True,
cwd=apk_folder, text=True,
capture_output=True)
# Check for issues
if "ERROR" in out.stdout or "EXCEPTION" in out.stdout:
print(f"FAILED: {patch} failed, please fix by hand.")
failures.append(patch)
out_path += "-failed"
if debug:
print(out.stdout)
# Apply patch to main APK folder too
if patch in ["necessary"]:
apply_patch(patch_contents)
if from_versionname != to_versionname:
# Add supported version to readme of that patch, hacky
# https://stackoverflow.com/a/35130508/3286892
with open(readme_path, 'r') as f:
readme_text = f.read().replace(f'- {from_versionname}\n', f'- {from_versionname}\n' f'- {to_versionname}\n')
with open(readme_path, "w") as f:
f.write(readme_text)
# Save ported patch
with open(out_path, "w") as f:
f.write(patch_contents)
if not out_path.endswith("-failed"):
print(f"PORTED: {patch} was successfully ported.")
for patch in os.listdir(os.path.join(cutthecord_folder, "resources/patches")):
if debug:
print(f"going over patch: {patch}")
# Ignore non-dirs
if not os.path.isdir(os.path.join(cutthecord_folder, "resources/patches", patch)):
if debug:
print(f"patch is not a folder, skipping: {patch}")
continue
pre_in_path = os.path.join(cutthecord_folder, "resources/patches", patch, f"{from_versioncode}-pre.sh")
post_in_path = os.path.join(cutthecord_folder, "resources/patches", patch, f"{from_versioncode}-post.sh")
pre_out_path = os.path.join(cutthecord_folder, "resources/patches", patch, f"{to_versioncode}-pre.sh")
post_out_path = os.path.join(cutthecord_folder, "resources/patches", patch, f"{to_versioncode}-post.sh")
patch_path = os.path.join(cutthecord_folder, "resources/patches", patch, f"{from_versioncode}.patch")
out_path = os.path.join(cutthecord_folder, "resources/patches", patch, f"{to_versioncode}.patch")
readme_path = os.path.join(cutthecord_folder, "resources/patches", patch, "README.md")
# Handle copying of versioned scripts, untested and dirty!
script_path = os.path.join(cutthecord_folder, "resources/patches", patch, f"{from_versioncode}.sh")
if os.path.exists(script_path):
script_out_path = os.path.join(cutthecord_folder, "resources/patches", patch, f"{to_versioncode}.sh")
with open(script_path) as f:
with open(script_out_path, "w") as f2:
f2.write(f.read())
# Check if patch exists for from_version, if it doesn't, warn user
if not os.path.isfile(patch_path) and patch not in ["necessary"]:
# Don't warn on instructional patches
if patch not in ["customfont", "customring", "bettertm", "bettertmlight", "blobs"]:
print(f"SKIPPED: No {from_versionname} version found for {patch}.")
continue
# Check if pre-script exists, if it does copy it
if os.path.isfile(pre_in_path):
shutil.copyfile(pre_in_path, pre_out_path)
print(f"PRE COPIED: {patch}'s pre script was copied.")
# Check if post-script exists, if it does copy it
if os.path.isfile(post_in_path):
shutil.copyfile(post_in_path, post_out_path)
print(f"POST COPIED: {patch}'s post script was copied.")
# Create necessary instead of porting it.
if patch == "necessary":
patch_contents = make_necessary(to_versioncode, to_versionname)
else:
# Get a modified version of the patch
patch_contents = modify_patch(patch, patch_path)
# Pass the new patch to patch command and get it to attempt to patch
out = subprocess.run("patch -p1 --dry-run --force", shell=True,
cwd=apk_folder, input=patch_contents, text=True,
capture_output=True)
# Check for issues
if "FAILED" in out.stdout or "can't find file to patch" in out.stdout:
print(f"FAILED: {patch} failed, please fix by hand.")
failures.append(patch)
out_path += "-failed"
elif "offset" in out.stdout:
temp_patch_contents = fix_offset(patch_contents)
if temp_patch_contents:
patch_contents = temp_patch_contents
print(f"WARNING: {patch} has offsets which were auto corrected.")
else:
print(f"FAILED: {patch} is missing files, please fix by hand.")
failures.append(patch)
out_path += "-failed"
if debug:
print(out.stdout)
# Apply patch to main APK folder too
if patch in ["necessary"]:
apply_patch(patch_contents)
if from_versionname != to_versionname:
# Add supported version to readme of that patch, hacky
# https://stackoverflow.com/a/35130508/3286892
with open(readme_path, 'r') as f:
readme_text = f.read().replace(f'- {from_versionname}\n', f'- {from_versionname}\n' f'- {to_versionname}\n')
with open(readme_path, "w") as f:
f.write(readme_text)
# Save ported patch
with open(out_path, "w") as f:
f.write(patch_contents)
if not out_path.endswith("-failed"):
print(f"PORTED: {patch} was successfully ported.")
ctcreadme_path = os.path.join(cutthecord_folder, "README.md")
# TODO: can we pull the correct date from distok?
out_datestamp = datetime.datetime.utcnow().strftime("%Y-%m-%d")
# Update readme with latest version, hacky
# https://stackoverflow.com/a/35130508/3286892
with open(ctcreadme_path, 'r') as f:
ctcr_text = f.read().replace(f'{from_versionname} ({from_versioncode})',
f'{to_versionname} ({to_versioncode})')
in_datestamp = re_releasedate.findall(ctcr_text)[0]
ctcr_text = ctcr_text.replace(in_datestamp, out_datestamp)
with open(ctcreadme_path, "w") as f:
f.write(ctcr_text)
with open("/opt/cutthecord/cutthecord/resources/patchport-state.json", "w") as f:
jout = {"versionname": to_versionname, "versioncode": to_versioncode}
json.dump(jout, f)
if failures:
print(f"Port complete. Following patches failed: {', '.join(failures)}")
else:
print("Port complete. All patches completed successfully.")