From b24319128fe509ac5a5b7c5ee03d2bd979ff536e Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Dominguez Date: Tue, 29 May 2018 10:21:39 +0900 Subject: [PATCH] Improvement - 335 - Automation --- app-template/bitcoincom/appConfig.json | 2 +- app-template/package-template.json | 1 + resources/bitcoin.com/mac/build.cfg | 32 +++ resources/bitcoin.com/mac/build_mas.py | 253 ++++++++++++++++++ .../bitcoin.com/mac/entitlements-child.plist | 10 + .../bitcoin.com/mac/entitlements-parent.plist | 16 ++ 6 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 resources/bitcoin.com/mac/build.cfg create mode 100755 resources/bitcoin.com/mac/build_mas.py create mode 100644 resources/bitcoin.com/mac/entitlements-child.plist create mode 100644 resources/bitcoin.com/mac/entitlements-parent.plist diff --git a/app-template/bitcoincom/appConfig.json b/app-template/bitcoincom/appConfig.json index 174adc6d9..8d450e25d 100644 --- a/app-template/bitcoincom/appConfig.json +++ b/app-template/bitcoincom/appConfig.json @@ -25,8 +25,8 @@ "pushSenderId": "1036948132229", "description": "A Secure Bitcoin Wallet", "version": "4.11.0", - "fullVersion": "4.11-rc1", "androidVersion": "411000", + "fullVersion": "4.11-rc1", "_extraCSS": "", "_enabledExtensions": { "coinbase": false, diff --git a/app-template/package-template.json b/app-template/package-template.json index 222bbcf7e..65c97f7e9 100644 --- a/app-template/package-template.json +++ b/app-template/package-template.json @@ -3,6 +3,7 @@ "description": "*DESCRIPTION*", "author": "BitPay", "version": "*VERSION*", + "androidVersion": "*ANDROIDVERSION*", "fullVersion": "*FULLVERSION*", "keywords": [ "bitcoin", diff --git a/resources/bitcoin.com/mac/build.cfg b/resources/bitcoin.com/mac/build.cfg new file mode 100644 index 000000000..c4c840b94 --- /dev/null +++ b/resources/bitcoin.com/mac/build.cfg @@ -0,0 +1,32 @@ +[Sign] +## [REQUIRED] Your Application Certificate Identity +ApplicationIdentity = 3rd Party Mac Developer Application: Saint Bitts LLC (299HJ3G3BP) +## [OPTIONAL] Parent entitlements file +ParentEntitlements = entitlements-parent.plist +## [OPTIONAL] Child entitlements file +ChildEntitlements = entitlements-child.plist +## [OPTIONAL] Sandbox. Default: Yes +Sandbox = Yes + +[Package] +## [REQUIRED for --pkg] Your Installer Certificate Identity +InstallerIdentity = 3rd Party Mac Developer Installer: Saint Bitts LLC (299HJ3G3BP) +## [OPTIONAL for --pkg] Installation path +InstallPath = /Applications + +[Info.plist] +## [OPTIONAL] Your app bundle identifier +CFBundleIdentifier = com.bitcoin.mwallet.mac +## [REQUIRED] Team ID obtained from Apple Developer -> Membership -> Team ID +NWTeamID = 299HJ3G3BP +## Properties of Info.plist will be overwritten in this section. + +[Resources] +## [OPTIONAL] Your custom icon file +Icon = /Users/jean-baptistedominguez/Documents/projects/wallet-ionic/resources/bitcoin.com/mac/app.icns +## [OPTIONAL] Locales +## If Locales is not set, all current locales are preserved. +## If comma separated locale list (e.g. en,fr,zh_CN) is given, you should have +## additional [Locale locale_name] section for each locale containing localized strings. +## Locales not in the list will be removed. +Locales = en \ No newline at end of file diff --git a/resources/bitcoin.com/mac/build_mas.py b/resources/bitcoin.com/mac/build_mas.py new file mode 100755 index 000000000..d067abacd --- /dev/null +++ b/resources/bitcoin.com/mac/build_mas.py @@ -0,0 +1,253 @@ +#!/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) + framework = glob(args.output, 'nwjs Framework.framework', returnOnFound=True) + system('codesign -f --verbose -s "%s" --entitlements %s --deep "%s"' % (identity, tmp_child_entitlements, framework)) + helperApp = glob(args.output, 'nwjs Helper.app', returnOnFound=True) + system('codesign -f --verbose -s "%s" --entitlements %s --deep "%s"' % (identity, tmp_child_entitlements, helperApp)) + + ## 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() diff --git a/resources/bitcoin.com/mac/entitlements-child.plist b/resources/bitcoin.com/mac/entitlements-child.plist new file mode 100644 index 000000000..635e25aac --- /dev/null +++ b/resources/bitcoin.com/mac/entitlements-child.plist @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.inherit + + + \ No newline at end of file diff --git a/resources/bitcoin.com/mac/entitlements-parent.plist b/resources/bitcoin.com/mac/entitlements-parent.plist new file mode 100644 index 000000000..12d6997e3 --- /dev/null +++ b/resources/bitcoin.com/mac/entitlements-parent.plist @@ -0,0 +1,16 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.application-groups + $GROUPID + com.apple.security.files.user-selected.read-only + + com.apple.security.network.client + + com.apple.security.device.camera + + +