Skip to content

Commit 2b3e79c

Browse files
Cong LiuLin Sun
authored andcommitted
Added building scripts for MAS
* build_mas.py * sample build.cfg * sample entitlements
1 parent 639ea50 commit 2b3e79c

File tree

4 files changed

+323
-0
lines changed

4 files changed

+323
-0
lines changed

tools/mas/build.cfg

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
[Sign]
2+
## [REQUIRED] Your Application Certificate Identity
3+
ApplicationIdentity = 3rd Party Mac Developer Application: Foo (XXXXXXXXXX)
4+
## [REQUIRED] (for --pkg) Your Installer Certificate Identity
5+
InstallerIdentity = 3rd Party Mac Developer Installer: Foo (XXXXXXXXXX)
6+
## [Optional] (for --pkg) Installation path
7+
InstallPath = /Applications
8+
## [REQUIRED] Entitlements
9+
ParentEntitlements = entitlements-parent.plist
10+
ChildEntitlements = entitlements-child.plist
11+
12+
[Info.plist]
13+
## [REQUIRED] Your app bundle identifier
14+
CFBundleIdentifier = your.app.bundle.id
15+
## [REQUIRED] Team ID obtained from Apple Developer -> Membership -> Team ID
16+
NWTeamID = XXXXXXXXXX
17+
## Properties of Info.plist will be overwritten in this section.
18+
19+
[Resources]
20+
## [OPTIONAL] Your custom icon file
21+
Icon = path/to/custom/icon.icns
22+
## [OPTIONAL] Locales
23+
## If Locales is not set, all current locales are preserved.
24+
## If comma separated locale list (e.g. en,fr,zh_CN) is given, you should have
25+
## additional [Locale locale_name] section for each locale containing localized strings.
26+
## Locales not in the list will be removed.
27+
Locales = en
28+
29+
## [OPTIONAL] custom locales
30+
[Locale en]
31+
CFBundleDisplayName = My App
32+
CFBundleGetInfoString = My App 1.0.0, Copyright 2016 My Company. All rights reserved.
33+
CFBundleName = My App
34+
NSHumanReadableCopyright = Copyright 2016 My Company. All rights reserved.

tools/mas/build_mas.py

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
#!/usr/bin/env python
2+
3+
import argparse
4+
import ConfigParser
5+
import shutil
6+
import os
7+
import fnmatch
8+
import plistlib
9+
import tempfile
10+
from datetime import datetime
11+
import sys
12+
import io
13+
14+
bundleid = None
15+
verbose = False
16+
17+
def info(msg):
18+
global verbose
19+
if verbose:
20+
print '[INFO] %s' % msg
21+
22+
def error(msg):
23+
print '[ERROR] %s' % msg
24+
print '\nFailed.'
25+
sys.exit(1)
26+
27+
def system(cmd):
28+
info(cmd)
29+
os.system(cmd)
30+
31+
def check_options(config, section, required_options, msg):
32+
missed_options = []
33+
34+
for option in required_options:
35+
if not config.has_option(section, option):
36+
missed_options.append(option)
37+
38+
if len(missed_options) != 0:
39+
error(msg % (section, ', '.join(missed_options)))
40+
41+
def glob(pathname, pattern, returnOnFound=False):
42+
matches = []
43+
for root, dirnames, filenames in os.walk(pathname):
44+
for dirname in fnmatch.filter(dirnames, pattern):
45+
if returnOnFound:
46+
return os.path.join(root, dirname)
47+
matches.append(os.path.join(root, dirname))
48+
for filename in fnmatch.filter(filenames, pattern):
49+
if returnOnFound:
50+
return os.path.join(root, filename)
51+
matches.append(os.path.join(root, filename))
52+
return matches
53+
54+
def get_bundle_id(args):
55+
global bundleid
56+
if bundleid is None:
57+
plist = plistlib.readPlist(os.path.join(args.output, 'Contents/Info.plist'))
58+
bundleid = plist['CFBundleIdentifier']
59+
return bundleid
60+
61+
def get_from_info_plist(args, key, default=None):
62+
plist = plistlib.readPlist(os.path.join(args.output, 'Contents/Info.plist'))
63+
if key in plist:
64+
return plist[key]
65+
else:
66+
return default
67+
68+
def patch_info_plist_file(file, replaces):
69+
plist = plistlib.readPlist(file)
70+
for (key, val) in replaces:
71+
plist[key] = val
72+
plistlib.writePlist(plist, file)
73+
74+
def generate_infoplist_strings_file(file, items):
75+
with io.open(file, 'w', encoding='utf-16') as fd:
76+
for item in items:
77+
fd.write(unicode('%s = "%s";\n' % item, 'utf-8'))
78+
79+
def read_config(args):
80+
print '\nParsing config file %s' % args.config_file
81+
if not os.path.isfile(args.config_file):
82+
error('%s does not exist' % args.config_file)
83+
config = ConfigParser.SafeConfigParser()
84+
config.optionxform = str # set to str to prevent transforming into lower cases
85+
config.read(args.config_file)
86+
check_options(config, 'Sign', ['ApplicationIdentity', 'ParentEntitlements', 'ChildEntitlements'], 'Missed options in [%s]: %s')
87+
if args.pkg:
88+
check_options(config, 'Sign', ['InstallerIdentity'], 'Missed options for --pkg in [%s]: %s')
89+
return config
90+
91+
def copy_to_output(args):
92+
print '\nCopying %s to %s' % (args.input, args.output)
93+
shutil.rmtree(args.output, ignore_errors=True)
94+
shutil.copytree(args.input, args.output, symlinks=True) # symblic links are required
95+
96+
def patch_info_plist(config, args):
97+
print '\nPatching Info.plist files'
98+
99+
replaces = []
100+
for (key, val) in config.items('Info.plist'):
101+
replaces.append((key, val))
102+
103+
file = os.path.join(args.output, 'Contents/Info.plist')
104+
info(file)
105+
patch_info_plist_file(file, replaces)
106+
107+
info_plist_files = glob(os.path.join(args.output, 'Contents/Versions'), 'Info.plist')
108+
for file in info_plist_files:
109+
if 'nwjs Framework' in file:
110+
tmp_replaces = [('CFBundleIdentifier', '%s.framework' % get_bundle_id(args))]
111+
elif 'nwjs Helper' in file:
112+
tmp_replaces = [('CFBundleIdentifier', '%s.helper' % get_bundle_id(args))]
113+
else:
114+
error('Cannot patch unknown Info.plist %s' % file)
115+
info(file)
116+
patch_info_plist_file(file, tmp_replaces)
117+
118+
def patch_locales(config, args):
119+
print '\nPatching locales'
120+
locales = config.get('Resources', 'Locales').split(',')
121+
removed_locales = []
122+
generated_locales = []
123+
for infoplist_strings_file in glob(os.path.join(args.output, 'Contents/Resources'), 'InfoPlist.strings'):
124+
locale_dir = os.path.dirname(infoplist_strings_file)
125+
(locale, _) = os.path.splitext(os.path.basename(locale_dir))
126+
if locale not in locales:
127+
removed_locales.append(locale)
128+
shutil.rmtree(locale_dir)
129+
elif config.has_section('Locale %s' % locale):
130+
generated_locales.append(locale)
131+
generate_infoplist_strings_file(infoplist_strings_file, config.items('Locale %s' % locale))
132+
else:
133+
error('Missing [Locale %s] section' % locale)
134+
135+
if len(generated_locales) > 0:
136+
info('Generated locales for %s' % ', '.join(generated_locales))
137+
if len(removed_locales) > 0:
138+
info('Removed locales for %s' % ', '.join(removed_locales))
139+
140+
removed_paks = []
141+
for local_pak in glob(os.path.join(args.output, 'Contents/Versions'), 'locale.pak'):
142+
locale_dir = os.path.dirname(local_pak)
143+
(locale, _) = os.path.splitext(os.path.basename(locale_dir))
144+
if locale != 'en' and locale not in locales:
145+
removed_paks.append(locale)
146+
shutil.rmtree(locale_dir)
147+
148+
if len(removed_paks) > 0:
149+
info('Removed .pak files for %s' % ', '.join(removed_locales))
150+
151+
def patch_icon(config, args):
152+
plist = plistlib.readPlist(os.path.join(args.output, 'Contents/Info.plist'))
153+
icon = os.path.join(os.path.dirname(args.config_file), config.get('Resources', 'Icon'))
154+
dest_icon = os.path.join(args.output, 'Contents/Resources/%s' % plist['CFBundleIconFile'])
155+
info('Copying icon from %s to %s' % (icon, dest_icon))
156+
shutil.copy2(icon, dest_icon)
157+
158+
def codesign_app(config, args):
159+
print '\nCodesigning'
160+
161+
bundleid = get_bundle_id(args)
162+
163+
identity = config.get('Sign', 'ApplicationIdentity')
164+
parent = config.get('Sign', 'ParentEntitlements')
165+
child = config.get('Sign', 'ChildEntitlements')
166+
167+
(_, tmp_parent_entitlements) = tempfile.mkstemp()
168+
parent_entitlements = plistlib.readPlist(parent)
169+
teamid = get_from_info_plist(args, 'NWTeamID', default=None)
170+
if teamid is None:
171+
groupid = bundleid
172+
else:
173+
groupid = '%s.%s' % (teamid, bundleid)
174+
175+
(_, tmp_child_entitlements) = tempfile.mkstemp()
176+
child_entitlements = plistlib.readPlist(child)
177+
plistlib.writePlist(child_entitlements, tmp_child_entitlements)
178+
info('Child entitlements: %s' % tmp_child_entitlements)
179+
framework = glob(args.output, 'nwjs Framework.framework', returnOnFound=True)
180+
system('codesign -f --verbose -s "%s" --entitlements %s --deep "%s"' % (identity, tmp_child_entitlements, framework))
181+
helperApp = glob(args.output, 'nwjs Helper.app', returnOnFound=True)
182+
system('codesign -f --verbose -s "%s" --entitlements %s --deep "%s"' % (identity, tmp_child_entitlements, helperApp))
183+
184+
parent_entitlements['com.apple.security.application-groups'] = [groupid]
185+
plistlib.writePlist(parent_entitlements, tmp_parent_entitlements)
186+
info('Parent entitlements: %s' % tmp_parent_entitlements)
187+
system('codesign -f --verbose -s "%s" --entitlements %s --deep "%s"' % (identity, tmp_parent_entitlements, args.output))
188+
189+
def productbuild(config, args):
190+
print '\nRunning productbuild'
191+
installer_identity = config.get('Sign', 'InstallerIdentity')
192+
if config.has_option('Sign', 'InstallPath'):
193+
install_path = config.get('Sign', 'InstallPath')
194+
else:
195+
install_path = '/Applications'
196+
system('productbuild --component "%s" "%s" --sign "%s" "%s"' % (args.output, install_path, installer_identity, args.pkg))
197+
198+
def main():
199+
parser = argparse.ArgumentParser(description='Signing tool for NW.js app')
200+
parser.add_argument('-C', '--config-file', default='build.cfg', help='config file. (default: build.cfg)')
201+
parser.add_argument('-I', '--input', default='nwjs.app', help='path to input app. (default: nwjs.app)')
202+
parser.add_argument('-O', '--output', default='nwjs_output.app', help='path to output app. (default: nwjs_output.app)')
203+
parser.add_argument('-S', '--sign-only', default=False, help='run codesign without patching the app. (default: False)', action='store_true')
204+
parser.add_argument('-P', '--pkg', default=None, help='run productbuild to generate .pkg after codesign. (default: None)')
205+
parser.add_argument('-V', '--verbose', default=False, help='display detailed information. (default: False)', action='store_true')
206+
args = parser.parse_args()
207+
208+
global verbose
209+
verbose = args.verbose
210+
211+
if args.sign_only:
212+
info('Running in Sign Only mode. Only [Sign] section is used in config file')
213+
214+
if args.pkg:
215+
info('--pkg is ignored in Sign Only mode.')
216+
217+
218+
# read config file
219+
config = read_config(args)
220+
221+
# make a copy
222+
copy_to_output(args)
223+
224+
if not args.sign_only:
225+
# patch Info.plist
226+
patch_info_plist(config, args)
227+
228+
# process resources & locales
229+
if config.has_section('Resources'):
230+
if config.has_option('Resources', 'Locales'):
231+
patch_locales(config, args)
232+
if config.has_option('Resources', 'Icon'):
233+
patch_icon(config, args)
234+
235+
# codesign
236+
codesign_app(config, args)
237+
238+
if not args.sign_only and args.pkg:
239+
productbuild(config, args)
240+
241+
print '\nDone.'
242+
243+
if __name__ == "__main__":
244+
main()

tools/mas/entitlements-child.plist

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>com.apple.security.app-sandbox</key>
6+
<true/>
7+
<key>com.apple.security.inherit</key>
8+
<true/>
9+
</dict>
10+
</plist>

tools/mas/entitlements-parent.plist

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<!-- [REQUIRED] Sandbox -->
6+
<key>com.apple.security.app-sandbox</key>
7+
<true/>
8+
<!-- [REQUIRED] IPC -->
9+
<key>com.apple.security.application-groups</key>
10+
<string>$GROUPID</string>
11+
12+
<!-- [OPTIONAL] Network -->
13+
<!--
14+
<key>com.apple.security.network.client</key>
15+
<true/>
16+
<key>com.apple.security.network.server</key>
17+
<true/>
18+
-->
19+
<!-- [OPTIONAL] Print -->
20+
<!--
21+
<key>com.apple.security.print</key>
22+
<true/>
23+
-->
24+
<!-- [OPTIONAL] File dialogs -->
25+
<!--
26+
<key>com.apple.security.files.user-selected.read-write</key>
27+
<true/>
28+
-->
29+
<!-- [OPTIONAL] Bluetooth -->
30+
<!--
31+
<key>com.apple.security.device.bluetooth</key>
32+
<true/>
33+
-->
34+
</dict>
35+
</plist>

0 commit comments

Comments
 (0)