2024-07-26 13:38:57 +00:00
|
|
|
import sys
|
|
|
|
import shutil
|
|
|
|
import os
|
|
|
|
from pathlib import Path
|
|
|
|
import subprocess
|
|
|
|
|
|
|
|
class BuildError(Exception):
|
|
|
|
def __init__(self):
|
|
|
|
super.__init__()
|
|
|
|
|
|
|
|
def getDexName(smaliName):
|
|
|
|
if smaliName == 'smali':
|
|
|
|
return 'classes.dex'
|
|
|
|
elif smaliName.startswith('smali_classes'):
|
|
|
|
return 'classes' + smaliName[13:] + '.dex'
|
|
|
|
print("Not a smali name")
|
|
|
|
raise BuildError()
|
|
|
|
|
|
|
|
class Task:
|
|
|
|
def clean():
|
|
|
|
shutil.rmtree('build')
|
|
|
|
|
|
|
|
def extractApk():
|
|
|
|
if Path('build/extracted').exists():
|
|
|
|
return
|
|
|
|
Path('build').mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
apk = list(filter(lambda f: f.endswith('.apk'), os.listdir('source-apk')))
|
|
|
|
if len(apk) == 0:
|
|
|
|
print('Please place the APK inside the source-apk directory.')
|
|
|
|
raise BuildError()
|
|
|
|
elif len(apk) > 1:
|
|
|
|
print('More than one apk found in source-apk. Please remove all but one.')
|
|
|
|
raise BuildError()
|
|
|
|
|
|
|
|
# Extract the apk
|
|
|
|
print("Extracting APK...")
|
|
|
|
subprocess.run(['java', '-jar', './tools/apktool_2.9.3.jar', 'd', 'source-apk/' + apk[0], '-o', 'build/extracted'])
|
|
|
|
|
|
|
|
def copySmali():
|
|
|
|
Task.extractApk()
|
|
|
|
|
|
|
|
Path('build/smali').mkdir(parents=True, exist_ok=True)
|
|
|
|
# Copy smali classes that haven't been yet
|
|
|
|
for file in os.listdir('build/extracted'):
|
|
|
|
if file != 'smali' and not file.startswith('smali_classes'):
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Skip if already exists
|
|
|
|
if Path(f'build/smali/{file}').exists():
|
|
|
|
continue
|
|
|
|
|
|
|
|
print(f'Copying {file}...')
|
|
|
|
shutil.copytree(f'build/extracted/{file}', f'build/smali/{file}')
|
|
|
|
|
|
|
|
# Copy from src
|
|
|
|
print('Copying ykit smali...')
|
|
|
|
shutil.copytree('src/smali', 'build/smali', dirs_exist_ok=True)
|
|
|
|
|
|
|
|
def compileSmali():
|
|
|
|
Task.copySmali()
|
|
|
|
|
|
|
|
Path('build/dex').mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
def shouldCompileSmali(file, dex):
|
|
|
|
src = f'build/smali/{file}'
|
|
|
|
ykitsrc = f'src/smali/{file}'
|
|
|
|
dst = f'build/dex/{dex}'
|
|
|
|
|
|
|
|
# If dest doesn't exist, always build
|
|
|
|
if not Path(dst).exists():
|
|
|
|
return True
|
|
|
|
|
|
|
|
# Check if ykit src dir exists for it, if not then don't recompile
|
|
|
|
if not Path(ykitsrc).exists():
|
|
|
|
return False
|
|
|
|
|
|
|
|
# Get latest file timestamp in ykitsrc
|
|
|
|
files = list(Path(ykitsrc).rglob('*.smali'))
|
|
|
|
files = [os.path.getmtime(x) for x in files]
|
|
|
|
latest = max(files)
|
|
|
|
|
|
|
|
dsttime = os.path.getmtime(dst)
|
|
|
|
|
|
|
|
# If it is newer than the dest, then do recompile
|
|
|
|
if latest > dsttime:
|
|
|
|
return True
|
|
|
|
|
|
|
|
# Otherwise, don't
|
|
|
|
return False
|
|
|
|
|
|
|
|
for file in os.listdir('build/smali'):
|
|
|
|
dex = getDexName(file)
|
|
|
|
|
|
|
|
if not shouldCompileSmali(file, dex):
|
|
|
|
print(f"Skipping {file}...")
|
|
|
|
continue
|
|
|
|
|
|
|
|
print(f"Compiling {file}...")
|
|
|
|
subprocess.run(['java', '-jar', 'tools/smali-2.5.2.jar', 'a', f'build/smali/{file}', '-o', f'build/dex/{dex}'])
|
|
|
|
|
2024-07-26 21:34:39 +00:00
|
|
|
def genClassPath():
|
|
|
|
print("Generating java classpath from dex files...")
|
|
|
|
for file in os.listdir('build/dex'):
|
|
|
|
src = f'build/dex/{file}'
|
|
|
|
dst = f'build/jar/{file[:-4]}.jar'
|
|
|
|
srctime = os.path.getmtime(src)
|
|
|
|
dsttime = 0 if not Path(dst).exists() else os.path.getmtime(dst)
|
|
|
|
|
|
|
|
if srctime < dsttime:
|
|
|
|
print(f"Skipping {file}...")
|
|
|
|
continue
|
|
|
|
|
|
|
|
print(f"Dex2jar {file}...")
|
|
|
|
subprocess.run(['java', '-cp', 'tools/d2j/*', 'com.googlecode.dex2jar.tools.Dex2jarCmd', '-f', src, '-o', dst])
|
|
|
|
|
|
|
|
def compileJava():
|
|
|
|
Task.genClassPath()
|
|
|
|
|
|
|
|
sep = ';' if os.name == 'nt' else ':'
|
|
|
|
javaFiles = [str(x) for x in Path('src/java').rglob('*.java')]
|
|
|
|
classPath = sep.join([str(x) for x in Path('build/jar').rglob('*.jar')])
|
|
|
|
|
|
|
|
# Get latest java file timestamp
|
|
|
|
files = list(Path('src/java').rglob('*.java'))
|
|
|
|
files = [os.path.getmtime(x) for x in files]
|
|
|
|
latest = max(files)
|
|
|
|
|
|
|
|
# Get latest dex time or 0
|
|
|
|
latest2 = 0 if not Path('build/java_dex/classes.dex').exists() else os.path.getmtime('build/java_dex/classes.dex')
|
|
|
|
|
|
|
|
# If dex file is newer than source, no need to do anything
|
|
|
|
if latest2 > latest:
|
|
|
|
print("Skipping java...")
|
|
|
|
return
|
|
|
|
|
|
|
|
print("Compiling java...")
|
|
|
|
subprocess.run(['javac', '-cp', classPath, '-d', 'build/java_class', *javaFiles])
|
|
|
|
|
|
|
|
classFiles = [str(x) for x in Path('build/java_class').rglob('*.class')]
|
|
|
|
|
|
|
|
Path('build/java_dex').mkdir(parents=True, exist_ok=True)
|
|
|
|
subprocess.run(['java', '-cp', 'tools/d8.jar', 'com.android.tools.r8.D8', *classFiles, '--output', 'build/java_dex'])
|
|
|
|
|
2024-07-26 16:25:35 +00:00
|
|
|
def mergeResources():
|
|
|
|
Task.compileSmali()
|
2024-07-26 21:34:39 +00:00
|
|
|
Task.compileJava()
|
2024-07-26 16:25:35 +00:00
|
|
|
|
|
|
|
# Copy original resources
|
|
|
|
# on first run only
|
|
|
|
if not Path('build/merged').exists():
|
|
|
|
print('Copying extracted resources...')
|
|
|
|
shutil.copytree('build/extracted', 'build/merged', dirs_exist_ok=True, ignore=shutil.ignore_patterns('smali', 'smali_classes*'))
|
|
|
|
|
|
|
|
print("Copying ykit resources...")
|
|
|
|
shutil.copytree('src/resources', 'build/merged', dirs_exist_ok=True)
|
|
|
|
|
|
|
|
print("Copying compiled dex files...")
|
|
|
|
shutil.copytree('build/dex', 'build/merged', dirs_exist_ok=True)
|
2024-07-26 21:34:39 +00:00
|
|
|
shutil.copy('build/java_dex/classes.dex', 'build/merged/classes7.dex') # TODO: Unhardcode this
|
2024-07-26 16:25:35 +00:00
|
|
|
|
|
|
|
def buildApk():
|
|
|
|
Task.mergeResources()
|
|
|
|
|
|
|
|
debugFlag = ('-d',) if ('d' in ''.join(sys.argv[2:])) else ()
|
|
|
|
|
|
|
|
print("Building apk...")
|
2024-07-26 17:12:39 +00:00
|
|
|
subprocess.run(['java', '-jar', './tools/apktool_2.9.3.jar', 'b', *debugFlag, 'build/merged', '-o', 'build/apk/tumblr-ykit-unsigned.apk'])
|
|
|
|
|
|
|
|
def alignApk():
|
|
|
|
Task.buildApk()
|
|
|
|
|
|
|
|
print('Aligning apk...')
|
|
|
|
subprocess.run(['tools/zipalign', '-p', '-f', '-v', '4', 'build/apk/tumblr-ykit-unsigned.apk', 'build/apk/tumblr-ykit.apk'])
|
|
|
|
|
|
|
|
def debugKey():
|
|
|
|
if Path('keystores/debug.keystore').exists():
|
|
|
|
return
|
|
|
|
|
|
|
|
print("Debug keystore does not exist, generating...")
|
|
|
|
subprocess.run(['keytool', '-genkey', '-v', '-keystore', 'keystores/debug.keystore', '-alias', 'alias_name', '-keyalg', 'RSA', '-keysize', '2048', '-validity', '10000', '-storepass', '123456', '-keypass', '123456', '-dname', 'CN=, OU=, O=, L=, ST=, C='])
|
|
|
|
|
|
|
|
def signApk():
|
|
|
|
Task.alignApk()
|
|
|
|
Task.debugKey()
|
|
|
|
|
|
|
|
debugFlag = 'd' in ''.join(sys.argv[2:])
|
|
|
|
releaseFlag = 'r' in ''.join(sys.argv[2:])
|
|
|
|
|
|
|
|
if not (debugFlag or releaseFlag):
|
|
|
|
print("Neither debug nor release flag was specified so the apk will not be signed.")
|
|
|
|
return
|
|
|
|
|
|
|
|
if releaseFlag:
|
|
|
|
if not Path('keystores/release.keystore').exists():
|
|
|
|
print("release.keystore is missing from keystores directory")
|
|
|
|
return
|
|
|
|
|
|
|
|
keystoreOpts = ('--ks', 'keystores/release.keystore',)
|
|
|
|
else:
|
|
|
|
Task.debugKey()
|
|
|
|
keystoreOpts = ('--ks', 'keystores/debug.keystore', '--ks-pass', 'pass:123456',)
|
|
|
|
|
|
|
|
print('Signing apk...')
|
|
|
|
subprocess.run(['java', '-jar', './tools/apksigner.jar', 'sign', *keystoreOpts, 'build/apk/tumblr-ykit.apk'])
|
|
|
|
print("Successfully signed apk in 'build/apk/tumblr-ykit.apk' using " + ('release' if releaseFlag else 'debug') + ' keystore')
|
2024-07-26 16:25:35 +00:00
|
|
|
|
|
|
|
def deployToAndroidStudio():
|
|
|
|
projDir = Path.home() / 'ApkProjects/tumblr-ykit'
|
|
|
|
if not projDir.exists():
|
|
|
|
print("No project named 'tumblr-ykit' in ~/ApkProjects/")
|
2024-07-26 17:12:39 +00:00
|
|
|
|
2024-07-26 16:25:35 +00:00
|
|
|
shutil.copy('build/apk/tumblr-ykit.apk', projDir / 'tumblr-ykit.apk')
|
|
|
|
print("Deployed to Android Studio")
|
|
|
|
|
2024-07-26 13:38:57 +00:00
|
|
|
def main():
|
|
|
|
|
|
|
|
match sys.argv[1]:
|
|
|
|
case 'clean':
|
|
|
|
Task.clean()
|
|
|
|
case 'extract':
|
|
|
|
Task.extractApk()
|
2024-07-26 17:12:39 +00:00
|
|
|
case 'assemble':
|
2024-07-26 16:25:35 +00:00
|
|
|
Task.buildApk()
|
2024-07-26 17:12:39 +00:00
|
|
|
case 'build':
|
|
|
|
Task.signApk()
|
|
|
|
|
2024-07-26 16:25:35 +00:00
|
|
|
if ('a' in ''.join(sys.argv[2:])):
|
|
|
|
Task.deployToAndroidStudio()
|
2024-07-26 21:34:39 +00:00
|
|
|
case 'genclasspath':
|
|
|
|
Task.genClassPath()
|
2024-07-26 13:38:57 +00:00
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|