uBlock/tools/build.py

270 lines
8.7 KiB
Python

#!/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',
'locales.json',
'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)