diff options
Diffstat (limited to 'tools/build.py')
-rw-r--r-- | tools/build.py | 263 |
1 files changed, 263 insertions, 0 deletions
diff --git a/tools/build.py b/tools/build.py new file mode 100644 index 0000000..d13a986 --- /dev/null +++ b/tools/build.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python3 + +import os +import re +import json +import glob +import sys +import subprocess +from time import strftime +from datetime import datetime +from shutil import which as iscmd, rmtree as rmt, copytree, copy, move +from collections import OrderedDict +from xml.sax.saxutils import escape + +osp = os.path +pj = osp.join + +os.chdir(pj(osp.split(osp.abspath(__file__))[0], '..')) + + +def rmtree(path): + if osp.exists(path): + rmt(path) + + +def mkdirs(path): + try: + os.makedirs(path) + finally: + return osp.exists(path) + + +def readfile(path, mode='rt'): + with open(path, mode) as f: + return f.read() + + +src_dir = osp.abspath(pj('src')) +meta_dir = osp.abspath(pj('meta')) +tmp_dir = osp.abspath(pj('tmp')) + +with open(pj(meta_dir, 'config.json'), encoding='utf-8') as f: + config = json.load(f) + +vendors = config['vendors'] +del config['vendors'] + +tmp = datetime.now() - datetime(year=datetime.today().year, month=1, day=1) +config['build_number'] = strftime('%y' + str(int(tmp.total_seconds() * 65535 / 31536000)).zfill(5)) + +descriptions = OrderedDict({}) +source_locale_dir = pj('src', '_locales') + +build_tmp = pj(tmp_dir, config['clean_name']) +build_dir = osp.abspath(pj('dist', 'build', config['version'])) + + +# fill 'descriptions' +for alpha2 in os.listdir(source_locale_dir): + with open(pj(source_locale_dir, alpha2, 'messages.json'), encoding='utf-8') as f: + string_data = json.load(f, object_pairs_hook=OrderedDict) + + descriptions[alpha2] = string_data['extShortDesc']['message'] + + +# only needed for Safari +with open(pj(src_dir, 'locales.json'), 'wt', encoding='utf-8', newline='\n') as f: + tmp = { + '_': config['def_lang'] + } + + for alpha2 in descriptions: + tmp[alpha2] = 1 + + json.dump(tmp, f, sort_keys=True, ensure_ascii=False) + + +with open(pj(src_dir, 'js', 'vapi-appinfo.js'), 'r+t', encoding='utf-8', newline='\n') as f: + tmp = f.read() + f.seek(0) + + f.write(re.sub( + r'/\*\*/([^:]+:).+', + lambda m: '/**/' + m.group(1) + " '" + config[m.group(1)[:-1]] + "',", + tmp + )) + + +with open(pj(src_dir, vendors['crx']['manifest']), 'wt', encoding='utf-8', newline='\n') as f: + cf_content = readfile(pj(meta_dir, 'crx', vendors['crx']['manifest'])) + + f.write( + re.sub(r"\{(?=\W)|(?<=\W)\}", r'\g<0>\g<0>', cf_content).format(**config) + ) + + +with open(pj(src_dir, vendors['safariextz']['manifest']['Info']), 'wt', encoding='utf-8', newline='\n') as f: + config['app_id'] = vendors['safariextz']['app_id'] + config['description'] = descriptions[config['def_lang']] + cf_content = readfile(pj(meta_dir, 'safariextz', vendors['safariextz']['manifest']['Info'])) + f.write(cf_content.format(**config)) + +copy(pj(meta_dir, 'safariextz', vendors['safariextz']['manifest']['Settings']), pj(src_dir, vendors['safariextz']['manifest']['Settings'])) + + +if 'meta' in sys.argv: + raise SystemExit('Metadata generated.') + + +rmtree(tmp_dir) +mkdirs(tmp_dir) + +rmtree(build_dir) +mkdirs(build_dir) + +# create update meta +for vendor, ext in {'crx': 'xml', 'safariextz': 'plist'}.items(): + with open(pj(build_dir, 'update_' + vendor + '.' + ext), 'wt', encoding='utf-8', newline='\n') as f: + if vendor == 'safariextz': + config['developer_identifier'] = vendors[vendor]['developer_identifier'] + + config['app_id'] = vendors[vendor]['app_id'] + cf_content = readfile(pj(meta_dir, vendor, 'update_' + vendor + '.' + ext)) + f.write(cf_content.format(**config)) + f.close() + + +# separate vendor specific code +for vapijsfile in [pj(src_dir, 'js', 'vapi-' + jsfile + '.js') for jsfile in ['background', 'common', 'client']]: + vapijs = readfile(vapijsfile) + + # "» name" is the start marker, "«" is the end marker + js_parts = re.findall(r'»\s*(\w+)\n([^«]+)//', vapijs) + + if not js_parts: + continue + + js_header = js_parts.pop(0)[1] + js_footer = js_parts.pop()[1] + + for js in js_parts: + with open(pj(tmp_dir, js[0] + '_' + osp.basename(vapijsfile)), 'wt', encoding='utf-8', newline='\n') as f: + f.write(js_header) + f.write(re.sub(r'^ ', '', js[1], flags=re.M)) + f.write(js_footer) + + +def move_vendor_specific_js(vendor): + for file in ['background', 'common', 'client']: + move(pj(tmp_dir, vendor + '_vapi-' + file + '.js'), pj(build_tmp, 'js', 'vapi-' + file + '.js')) + + +def copy_vendor_files(files): + for file in files: + path = pj(src_dir, file) + + if osp.isdir(path): + copytree(path, pj(build_tmp, file), copy_function=copy) + else: + copy(path, pj(build_tmp, file)) + + +def remove_vendor_files(files): + for file in files: + path = pj(build_tmp, file) + + if osp.isdir(path): + rmtree(path) + else: + os.remove(path) + + +def norm_cygdrive(path): + return '/cygdrive/' + path[0] + path[2:].replace('\\', '/') if path[1] == ':' else path + + +mkdirs(build_tmp) + +for file in glob.iglob(pj(src_dir, '*')): + basename = osp.basename(file) + + if osp.isfile(file) and (file.endswith('.html') or basename == 'icon.png'): + copy(file, pj(build_tmp, basename)) + elif osp.isdir(file) and basename not in ['_locales', 'locale']: + copytree(file, pj(build_tmp, basename), copy_function=copy) + +os.remove(pj(build_tmp, 'js', 'sitepatch-safari.js')) + + +package_name = config['clean_name'] + '-' + config['version'] + + +# Chrome +if not iscmd('7z'): + print('Cannot build for Chrome: `7z` command not found.') +else: + vendor_files = ['_locales', 'manifest.json'] + + move_vendor_specific_js('crx') + copy_vendor_files(vendor_files) + + package = pj(build_dir, package_name + '.zip') + subprocess.call('7z a -r -tzip -mx=8 "' + norm_cygdrive(package) + '" "' + norm_cygdrive(pj(build_tmp, '*')) + '"', stdout=subprocess.DEVNULL) + + if osp.exists(vendors['crx']['private_key']): + if not iscmd('openssl'): + print('Cannot build for Chrome: `openssl` command not found.') + else: + # Convert the PEM key to DER (and extract the public form) for inclusion in the CRX header + derkey = subprocess.Popen([ + 'openssl', 'rsa', '-pubout', + '-inform', 'PEM', + '-outform', 'DER', + '-in', norm_cygdrive(vendors['crx']['private_key']) + ], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.read() + # Sign the zip file with the private key in PEM format + signature = subprocess.Popen([ + 'openssl', 'sha1', + '-sign', norm_cygdrive(vendors['crx']['private_key']), + norm_cygdrive(package) + ], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.read() + out = open(package.replace('.zip', vendors['crx']['file_ext']), "wb") + # Extension file magic number + out.write(bytes("Cr24\x02\x00\x00\x00", 'UTF-8') + len(derkey).to_bytes(4, 'little') + len(signature).to_bytes(4, 'little')) + out.write(derkey) + out.write(signature) + out.write(readfile(package, 'rb')) + out.close() + + subprocess.call('7z a ' + norm_cygdrive(package) + ' ' + norm_cygdrive(osp.abspath(vendors['crx']['private_key'])), stdout=subprocess.DEVNULL) + + remove_vendor_files(vendor_files) + + +# Safari +if not iscmd('xar'): + print('Cannot build for Safari: `xar` command not found.') +elif osp.exists(vendors['safariextz']['cert_dir']): + vendor_files = ['_locales', 'Info.plist', 'Settings.plist', pj('js', 'sitepatch-safari.js')] + + move_vendor_specific_js('safariextz') + copy_vendor_files(vendor_files) + + build_tmp = move(build_tmp, pj(tmp_dir, config['clean_name'] + '.safariextension')) + + # xar accepts only unix style directory separators + package = pj(build_dir, package_name + vendors['safariextz']['file_ext']).replace('\\', '/'); + subprocess.call('xar -czf "' + package + '" --compression-args=9 --distribution --directory="' + osp.basename(tmp_dir) + '" ' + config['clean_name'] + '.safariextension', stderr=subprocess.DEVNULL) + subprocess.call('xar --sign -f "' + package + '" --digestinfo-to-sign sfr_digest.dat --sig-size 256 ' + ' '.join('--cert-loc="' + vendors['safariextz']['cert_dir'] + 'cert0{0}"'.format(i) for i in range(3)), stderr=subprocess.DEVNULL) + subprocess.call('openssl rsautl -sign -inkey ' + vendors['safariextz']['private_key'] + ' -in sfr_digest.dat -out sfr_sig.dat', stderr=subprocess.DEVNULL) + subprocess.call('xar --inject-sig sfr_sig.dat -f "' + package + '"', stderr=subprocess.DEVNULL) + + os.remove('sfr_sig.dat') + os.remove('sfr_digest.dat') + + build_tmp = move(build_tmp, pj(tmp_dir, config['clean_name'])) + + remove_vendor_files(vendor_files) + + +rmtree(tmp_dir) + +print("Files ready @ " + build_dir) |