259 lines
9.5 KiB
Python
Executable file
259 lines
9.5 KiB
Python
Executable file
#!/usr/bin/env python
|
|
|
|
import argparse
|
|
import ConfigParser
|
|
import shutil
|
|
import os
|
|
import fnmatch
|
|
import plistlib
|
|
import tempfile
|
|
from datetime import datetime
|
|
import sys
|
|
import io
|
|
|
|
bundleid = None
|
|
verbose = False
|
|
|
|
def info(msg):
|
|
global verbose
|
|
if verbose:
|
|
print '[INFO] %s' % msg
|
|
|
|
def error(msg):
|
|
print '[ERROR] %s' % msg
|
|
print '\nFailed.'
|
|
sys.exit(1)
|
|
|
|
def system(cmd):
|
|
info(cmd)
|
|
os.system(cmd)
|
|
|
|
def check_options(config, section, required_options, msg):
|
|
missed_options = []
|
|
|
|
for option in required_options:
|
|
if not config.has_option(section, option):
|
|
missed_options.append(option)
|
|
|
|
if len(missed_options) != 0:
|
|
error(msg % (section, ', '.join(missed_options)))
|
|
|
|
def glob(pathname, pattern, returnOnFound=False):
|
|
matches = []
|
|
for root, dirnames, filenames in os.walk(pathname):
|
|
for dirname in fnmatch.filter(dirnames, pattern):
|
|
if returnOnFound:
|
|
return os.path.join(root, dirname)
|
|
matches.append(os.path.join(root, dirname))
|
|
for filename in fnmatch.filter(filenames, pattern):
|
|
if returnOnFound:
|
|
return os.path.join(root, filename)
|
|
matches.append(os.path.join(root, filename))
|
|
return matches
|
|
|
|
def get_bundle_id(args):
|
|
global bundleid
|
|
if bundleid is None:
|
|
plist = plistlib.readPlist(os.path.join(args.output, 'Contents/Info.plist'))
|
|
bundleid = plist['CFBundleIdentifier']
|
|
return bundleid
|
|
|
|
def get_from_info_plist(args, key, default=None):
|
|
plist = plistlib.readPlist(os.path.join(args.output, 'Contents/Info.plist'))
|
|
if key in plist:
|
|
return plist[key]
|
|
else:
|
|
return default
|
|
|
|
def patch_info_plist_file(file, replaces):
|
|
plist = plistlib.readPlist(file)
|
|
for (key, val) in replaces:
|
|
plist[key] = val
|
|
plistlib.writePlist(plist, file)
|
|
|
|
def generate_infoplist_strings_file(file, items):
|
|
with io.open(file, 'w', encoding='utf-16') as fd:
|
|
for item in items:
|
|
fd.write(unicode('%s = "%s";\n' % item, 'utf-8'))
|
|
|
|
def read_config(args):
|
|
print '\nParsing config file %s' % args.config_file
|
|
if not os.path.isfile(args.config_file):
|
|
error('%s does not exist' % args.config_file)
|
|
config = ConfigParser.SafeConfigParser()
|
|
config.optionxform = str # set to str to prevent transforming into lower cases
|
|
config.read(args.config_file)
|
|
check_options(config, 'Sign', ['ApplicationIdentity'], 'Missed options in [%s]: %s')
|
|
if args.pkg:
|
|
check_options(config, 'Package', ['InstallerIdentity'], 'Missed options for --pkg in [%s]: %s')
|
|
return config
|
|
|
|
def copy_to_output(args):
|
|
print '\nCopying %s to %s' % (args.input, args.output)
|
|
shutil.rmtree(args.output, ignore_errors=True)
|
|
shutil.copytree(args.input, args.output, symlinks=True) # symblic links are required
|
|
|
|
def patch_info_plist(config, args):
|
|
print '\nPatching Info.plist files'
|
|
|
|
replaces = []
|
|
for (key, val) in config.items('Info.plist'):
|
|
replaces.append((key, val))
|
|
|
|
file = os.path.join(args.output, 'Contents/Info.plist')
|
|
info(file)
|
|
patch_info_plist_file(file, replaces)
|
|
|
|
info_plist_files = glob(os.path.join(args.output, 'Contents/Versions'), 'Info.plist')
|
|
for file in info_plist_files:
|
|
if 'nwjs Framework' in file:
|
|
tmp_replaces = [('CFBundleIdentifier', '%s.framework' % get_bundle_id(args))]
|
|
elif 'nwjs Helper' in file:
|
|
tmp_replaces = [('CFBundleIdentifier', '%s.helper' % get_bundle_id(args))]
|
|
else:
|
|
error('Cannot patch unknown Info.plist %s' % file)
|
|
info(file)
|
|
patch_info_plist_file(file, tmp_replaces)
|
|
|
|
def patch_locales(config, args):
|
|
print '\nPatching locales'
|
|
locales = config.get('Resources', 'Locales').split(',')
|
|
removed_locales = []
|
|
generated_locales = []
|
|
for infoplist_strings_file in glob(os.path.join(args.output, 'Contents/Resources'), 'InfoPlist.strings'):
|
|
locale_dir = os.path.dirname(infoplist_strings_file)
|
|
(locale, _) = os.path.splitext(os.path.basename(locale_dir))
|
|
if locale not in locales:
|
|
removed_locales.append(locale)
|
|
shutil.rmtree(locale_dir)
|
|
elif config.has_section('Locale %s' % locale):
|
|
generated_locales.append(locale)
|
|
generate_infoplist_strings_file(infoplist_strings_file, config.items('Locale %s' % locale))
|
|
else:
|
|
error('Missing [Locale %s] section' % locale)
|
|
|
|
if len(generated_locales) > 0:
|
|
info('Generated locales for %s' % ', '.join(generated_locales))
|
|
if len(removed_locales) > 0:
|
|
info('Removed locales for %s' % ', '.join(removed_locales))
|
|
|
|
removed_paks = []
|
|
for local_pak in glob(os.path.join(args.output, 'Contents/Versions'), 'locale.pak'):
|
|
locale_dir = os.path.dirname(local_pak)
|
|
(locale, _) = os.path.splitext(os.path.basename(locale_dir))
|
|
if locale != 'en' and locale not in locales:
|
|
removed_paks.append(locale)
|
|
shutil.rmtree(locale_dir)
|
|
|
|
if len(removed_paks) > 0:
|
|
info('Removed .pak files for %s' % ', '.join(removed_locales))
|
|
|
|
def patch_icon(config, args):
|
|
plist = plistlib.readPlist(os.path.join(args.output, 'Contents/Info.plist'))
|
|
icon = os.path.join(os.path.dirname(args.config_file), config.get('Resources', 'Icon'))
|
|
dest_icon = os.path.join(args.output, 'Contents/Resources/%s' % plist['CFBundleIconFile'])
|
|
info('Copying icon from %s to %s' % (icon, dest_icon))
|
|
shutil.copy2(icon, dest_icon)
|
|
|
|
def codesign_app(config, args):
|
|
print '\nCodesigning'
|
|
|
|
bundleid = get_bundle_id(args)
|
|
|
|
identity = config.get('Sign', 'ApplicationIdentity')
|
|
sandbox = True
|
|
if config.has_option('Sign', 'Sandbox'):
|
|
sandbox = config.getboolean('Sign', 'Sandbox')
|
|
|
|
## sign child frameworks and helpers
|
|
(_, tmp_child_entitlements) = tempfile.mkstemp()
|
|
if config.has_option('Sign', 'ChildEntitlements'):
|
|
child = config.get('Sign', 'ChildEntitlements')
|
|
child_entitlements = plistlib.readPlist(child)
|
|
else:
|
|
child_entitlements = {
|
|
'com.apple.security.app-sandbox' : sandbox,
|
|
'com.apple.security.inherit' : True
|
|
}
|
|
|
|
plistlib.writePlist(child_entitlements, tmp_child_entitlements)
|
|
info('Child entitlements: %s' % tmp_child_entitlements)
|
|
|
|
libffmpeg = glob(os.path.join(args.output, 'Contents/Versions/55.0.2883.87/nwjs Framework.framework/Versions/A'), 'libffmpeg.dylib', returnOnFound=True)
|
|
system('codesign --deep --force --verbose --verify --sign "%s" --entitlements %s --deep "%s"' % (identity, tmp_child_entitlements, libffmpeg))
|
|
libnode = glob(os.path.join(args.output, 'Contents/Versions/55.0.2883.87/nwjs Framework.framework/Versions/A'), 'libnode.dylib', returnOnFound=True)
|
|
system('codesign --deep --force --verbose --verify --sign "%s" --entitlements %s --deep "%s"' % (identity, tmp_child_entitlements, libnode))
|
|
|
|
helperApp = glob(args.output, 'nwjs Helper.app', returnOnFound=True)
|
|
system('codesign --deep --force --verbose --verify --sign "%s" --entitlements %s --deep "%s"' % (identity, tmp_child_entitlements, helperApp))
|
|
framework = glob(args.output, 'nwjs Framework.framework', returnOnFound=True)
|
|
system('codesign --deep --force --verbose --verify --sign "%s" --entitlements %s --deep "%s"' % (identity, tmp_child_entitlements, framework))
|
|
|
|
## sign parent app
|
|
(_, tmp_parent_entitlements) = tempfile.mkstemp()
|
|
if config.has_option('Sign', 'ParentEntitlements'):
|
|
parent = config.get('Sign', 'ParentEntitlements')
|
|
parent_entitlements = plistlib.readPlist(parent)
|
|
else:
|
|
parent_entitlements = {}
|
|
teamid = get_from_info_plist(args, 'NWTeamID', default=None)
|
|
if teamid is None:
|
|
groupid = bundleid
|
|
else:
|
|
groupid = '%s.%s' % (teamid, bundleid)
|
|
parent_entitlements['com.apple.security.app-sandbox'] = sandbox
|
|
parent_entitlements['com.apple.security.application-groups'] = [groupid]
|
|
plistlib.writePlist(parent_entitlements, tmp_parent_entitlements)
|
|
|
|
info('Parent entitlements: %s' % tmp_parent_entitlements)
|
|
system('codesign -f --verbose -s "%s" --entitlements %s --deep "%s"' % (identity, tmp_parent_entitlements, args.output))
|
|
|
|
def productbuild(config, args):
|
|
print '\nRunning productbuild'
|
|
installer_identity = config.get('Package', 'InstallerIdentity')
|
|
if config.has_option('Package', 'InstallPath'):
|
|
install_path = config.get('Package', 'InstallPath')
|
|
else:
|
|
install_path = '/Applications'
|
|
system('productbuild --component "%s" "%s" --sign "%s" "%s"' % (args.output, install_path, installer_identity, args.pkg))
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='Signing tool for NW.js app')
|
|
parser.add_argument('-C', '--config-file', default='build.cfg', help='config file. (default: build.cfg)')
|
|
parser.add_argument('-I', '--input', default='nwjs.app', help='path to input app. (default: nwjs.app)')
|
|
parser.add_argument('-O', '--output', default='nwjs_output.app', help='path to output app. (default: nwjs_output.app)')
|
|
parser.add_argument('-S', '--skip-patching', default=False, help='run codesign without patching the app. (default: False)', action='store_true')
|
|
parser.add_argument('-P', '--pkg', default=None, help='run productbuild to generate .pkg after codesign. (default: None)')
|
|
parser.add_argument('-V', '--verbose', default=False, help='display detailed information. (default: False)', action='store_true')
|
|
args = parser.parse_args()
|
|
|
|
global verbose
|
|
verbose = args.verbose
|
|
|
|
# read config file
|
|
config = read_config(args)
|
|
|
|
# make a copy
|
|
copy_to_output(args)
|
|
|
|
if not args.skip_patching:
|
|
# patch Info.plist
|
|
patch_info_plist(config, args)
|
|
|
|
# process resources & locales
|
|
if config.has_section('Resources'):
|
|
if config.has_option('Resources', 'Locales'):
|
|
patch_locales(config, args)
|
|
if config.has_option('Resources', 'Icon'):
|
|
patch_icon(config, args)
|
|
|
|
# codesign
|
|
codesign_app(config, args)
|
|
|
|
if args.pkg:
|
|
productbuild(config, args)
|
|
|
|
print '\nDone.'
|
|
|
|
if __name__ == "__main__":
|
|
main()
|