Merge lp:~mikemc/ubuntuone-windows-installer/package-everything into lp:ubuntuone-windows-installer

Proposed by Mike McCracken
Status: Merged
Approved by: Manuel de la Peña
Approved revision: 134
Merged at revision: 127
Proposed branch: lp:~mikemc/ubuntuone-windows-installer/package-everything
Merge into: lp:ubuntuone-windows-installer
Diff against target: 462 lines (+253/-92)
1 file modified
scripts/setup-mac.py (+253/-92)
To merge this branch: bzr merge lp:~mikemc/ubuntuone-windows-installer/package-everything
Reviewer Review Type Date Requested Status
Manuel de la Peña (community) Approve
Alejandro J. Cura (community) Approve
Review via email: mp+115596@code.launchpad.net

Commit message

- Package all helper apps, use symlinks to share resources (LP: #1025342)

Description of the change

- Package all helper apps, use symlinks to share resources (LP: #1025342)

This also includes a couple other changes, including some code cleanup.

NOTE to testers:
The script currently expects to be run from a branch of ubuntuone-windows-installer inside the parts/ directory of a buildout. However, because the buildout environment itself is a checkout of the windows installer code, this is a little silly. It dates from before I understood the buildout.
That is fixed, but saved for a future merge.

To run with successful feelings, you need PYTHONPATH to include:
- a built copy of lp:~diego.sarmentero/+junk/python-macfsevents
- current trunk dirspec
- the same directory as setup-mac.py (to get conf.py)
- current trunk of ubuntu-sso-client, ubuntuone-client, and ubuntuone-storage-protocol.

You will also need to run setup-mac using the buildout python.

To run the resulting executable, you can do the following:

% XDG_DATA_HOME=~/Documents/Canonical/Source/buildout-env/scripts/devsetup/parts/ubuntuone-storage-protocol/data XDG_CONFIG_HOME=/Users/mmccrack/Documents/Canonical/Source/buildout-env/scripts/devsetup/parts/ubuntuone-client/data/ U1_DEBUG=1 dist/ubuntuone-control-panel-qt/UbuntuOne.app/Contents/MacOS/ubuntuone-control-panel-qt

However, with current trunk of storage-protocol as of this writing, you will see errors trying to find the SSL certs in /etc/ssl. See bug #1025950 for progress on that issue.

To post a comment you must log in.
Revision history for this message
Brian Curtin (brian.curtin) wrote :

I'll leave the actual approval to someone who has knowledge of the deeper Mac things in here, so just entering as a comment for now, but on the surface nothing jumps out as being incorrect as just general Python code.

99 + print "WARNING: got OSError %r in rmtree(%r)." \
100 + % (e, os.path.join(INSTALL_DIR, "lib"))

Could you wrap this in parentheses rather than using the '\' line continuation?

130. By Mike McCracken

style fix and comment-out the right unused executable

Revision history for this message
Alejandro J. Cura (alecu) wrote :

Great branch!

review: Approve
Revision history for this message
Manuel de la Peña (mandel) wrote :

Some stupid comments:

Is there a errno in a constant to be user here:

98 + if errno != 2:

is better than simply using 2, right?

What happens in '.py' is already present, we might as well test for it just in case:

157 + mainscript = os.path.join("bin", exename + ".py")

Shall we just remove the following:

189 + #NOTE: Disabled because this is not used as of 07/18/2012.
190 + # sso_ssl_app_path = do_setup(True,
191 + # "com.ubuntu.sso.ssl-cert",
192 + # "ubuntu-sso-ssl-certificate-qt")

Does it really give any required info for the next maintainers?

In the following:

406 + # copy everything in resources that isn't include, lib, or *.app:
407 + resources_to_copy = [p for p in helper_src_resources if
408 + p not in ['include', 'lib']]
409 +
410 + for rsrc_name in resources_to_copy:
411 + shutil.copy(os.path.join(helper_src_resources_path, rsrc_name),
412 + os.path.join(helper_dest_resources_path, rsrc_name))
413 +
414 + # link to resources/include and resources/lib from the main bundle:
415 + resources_to_link = [os.path.join("Resources", p)
416 + for p in ['include', 'lib']]

['include', 'lib'] are the include and resources, would it be a good idea to make it a constants so that we don't have to chance it in the code. That way, if we have too add other strings we just have to modify it in a single place. Maybe the 'Resources' stirng is also a good candidate for a constant.

In the following:

337 + if len(dynload_paths) != 1:
338 + raise Exception("can't expand ", dynload_glob)

maybe len(dynload_paths) < 1 or len(dynload_paths) == 0, I don't exactly know if we should fail also if the length is bigger.

Besides the above I ran the script and everything seems to be ok.

review: Needs Fixing
131. By Mike McCracken

Use errno constant for readability

132. By Mike McCracken

Add constant for Resources/lib and Resources/include

133. By Mike McCracken

Remove unused packaging code for sso-ssl-certificate-qt

134. By Mike McCracken

Improve error messages for globbing

Revision history for this message
Mike McCracken (mikemc) wrote :

> Some stupid comments:
>
> Is there a errno in a constant to be user here:
>
> 98 + if errno != 2:
>
> is better than simply using 2, right?

Good point - I forgot about the errno module.
I've changed it to check against errno.ENOENT.

> What happens in '.py' is already present, we might as well test for it just in
> case:
>
> 157 + mainscript = os.path.join("bin", exename + ".py")

The bin/exename.py file is written as part of the prepare step, see line 210 (of the file).

In the line you quoted, it could be there (and about to get overwritten) or not there (and that's OK).

If the file isn't there and you run 'python setup-mac.py py2app' instead of 'python setup-mac.py prepare py2app', you will get a pretty readable error from py2app about it.
It's not simple for us to check it here, though, because it might be OK for it to be missing - if we are going to do the prepare step and it's the first time we're calling setup() on this run of the script.

I've left it as-is, let me know if you think it should do more.

> Shall we just remove the following:
>
> 189 + #NOTE: Disabled because this is not used as of 07/18/2012.
> 190 + # sso_ssl_app_path = do_setup(True,
> 191 + # "com.ubuntu.sso.ssl-cert",
> 192 + # "ubuntu-sso-ssl-certificate-qt")
>
> Does it really give any required info for the next maintainers?

Not really, removed.

> In the following:
>
> 406 + # copy everything in resources that isn't include, lib, or *.app:
> 407 + resources_to_copy = [p for p in helper_src_resources if
> 408 + p not in ['include', 'lib']]
> 409 +
> 410 + for rsrc_name in resources_to_copy:
> 411 + shutil.copy(os.path.join(helper_src_resources_path, rsrc_name),
> 412 + os.path.join(helper_dest_resources_path, rsrc_name))
> 413 +
> 414 + # link to resources/include and resources/lib from the main bundle:
> 415 + resources_to_link = [os.path.join("Resources", p)
> 416 + for p in ['include', 'lib']]
>
> ['include', 'lib'] are the include and resources, would it be a good idea to
> make it a constants so that we don't have to chance it in the code. That way,
> if we have too add other strings we just have to modify it in a single place.

Agreed. I've made that change.

> Maybe the 'Resources' stirng is also a good candidate for a constant.

I didn't do this, because "Resources" will never change.

> In the following:
>
> 337 + if len(dynload_paths) != 1:
> 338 + raise Exception("can't expand ", dynload_glob)
>
> maybe len(dynload_paths) < 1 or len(dynload_paths) == 0, I don't exactly know
> if we should fail also if the length is bigger.

We should fail if the length is bigger, because then we don't know which one to use.
Maybe we'll end up packaging modules from multiple python versions at some point and then we'll need to make more changes to handle copying stuff from each of them. For now this code should break if it's != 1, though.

However, it could use a more descriptive error message. I've changed that.

> Besides the above I ran the script and everything seems to be ok.

Great!

Revision history for this message
Manuel de la Peña (mandel) wrote :

Cool, approving.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'scripts/setup-mac.py'
2--- scripts/setup-mac.py 2012-07-13 19:02:48 +0000
3+++ scripts/setup-mac.py 2012-07-25 20:04:22 +0000
4@@ -31,11 +31,13 @@
5
6
7 import copy
8+import errno
9 import glob
10 import os
11 import shutil
12 import subprocess
13 import sys
14+from zipfile import ZipFile
15
16 import conf
17
18@@ -51,6 +53,15 @@
19 sys.exit()
20
21
22+APP_NAMES = {'ubuntu-sso-login': 'Ubuntu SSO Helper',
23+ 'ubuntu-sso-login-qt': 'Ubuntu Single Sign-On',
24+ 'ubuntu-sso-ssl-certificate-qt': 'Ubuntu SSO SSL Certificate',
25+ 'ubuntu-sso-proxy-creds-qt': 'Ubuntu SSO Proxy Credentials',
26+ 'ubuntuone-syncdaemon': 'UbuntuOne Syncdaemon',
27+ 'ubuntuone-control-panel-qt': 'UbuntuOne',
28+ 'ubuntuone-proxy-tunnel': 'UbuntuOne Proxy Tunnel',
29+ }
30+
31 LOG_LEVEL = "DEBUG"
32 LOG_FILE_SIZE = "1000000"
33
34@@ -86,12 +97,38 @@
35
36 # pylint is complaining about parent classes having too many methods
37 # pylint: disable=R0904
38+class Clean(Command):
39+ """Remove built products from previous runs."""
40+
41+ user_options = []
42+
43+ def initialize_options(self):
44+ """No-op."""
45+
46+ def finalize_options(self):
47+ """No-op."""
48+
49+ def run(self):
50+ """Clean up."""
51+ for d in [INSTALL_DIR, 'dist', 'build']:
52+ if os.path.exists(d):
53+ print "Clean: removing", d
54+ shutil.rmtree(d)
55+ print "NOTE: exiting. 'clean' cannot be chained, "
56+ print " because we call setup() multiple times."
57+ sys.exit()
58+
59+# pylint: enable=R0904
60+
61+
62+# pylint is complaining about parent classes having too many methods
63+# pylint: disable=R0904
64 class DoNotPrepareAnything(Command):
65 """No-op, lets us call setup multiple times without re-preparing."""
66
67- user_options = user_options = [("from-trunk", None, "noop."),
68- ("only-prepare=", None, "noop."),
69- ("source-dir=", None, "noop.")]
70+ user_options = [("from-trunk", None, "noop."),
71+ ("only-prepare=", None, "noop."),
72+ ("source-dir=", None, "noop.")]
73
74 def initialize_options(self):
75 """No-op."""
76@@ -101,11 +138,9 @@
77
78 def finalize_options(self):
79 """No-op."""
80- pass
81
82 def run(self):
83 """No-op."""
84- pass
85
86 # pylint: enable=R0904
87
88@@ -156,7 +191,7 @@
89
90 print "Starting Prepare step:"
91
92- for folder in ["bin", INSTALL_DIR, "data"]:
93+ for folder in ["bin", INSTALL_DIR, "data", "build"]:
94 if not os.path.isdir(folder):
95 os.mkdir(folder)
96
97@@ -216,8 +251,10 @@
98 try:
99 shutil.rmtree(os.path.join(INSTALL_DIR, "lib"))
100 except OSError, e:
101- print "WARNING: OSError %r removing %r" \
102- % (e, os.path.join(INSTALL_DIR, "lib"))
103+ error, msg = e
104+ if error != errno.ENOENT:
105+ print ("WARNING: got OSError %r in rmtree(%r)."
106+ % (e, os.path.join(INSTALL_DIR, "lib")))
107
108 start_dir = os.getcwd()
109
110@@ -407,32 +444,48 @@
111 "/imageformats/*.dylib")
112 dylib_paths = qt_imageformat_plugins + qt_network_plugins
113
114+ data_files = glob.glob("data/*")
115+
116 master_plist = Plist.fromFile('data/macapp_template.plist')
117 master_options = {"includes": ['google.protobuf.descriptor',
118 'sip',
119 'twisted.web.resource',
120 'twisted.web.client',
121- 'ubuntu_sso.qt',
122- 'ubuntu_sso.qt.ui',
123- 'ubuntuone.storageprotocol',
124- 'oauth'],
125+ 'oauth'
126+ ],
127 "excludes": ["fsm", "PyQt4.uic"],
128 "frameworks": dylib_paths,
129- "resources": ['data/qt.conf'],
130+ "resources": data_files,
131 "strip": True,
132 }
133
134 # make sure we can see the prepared libraries:
135 sys.path.insert(0, os.path.join(INSTALL_DIR, "lib", "site-packages"))
136
137- def do_setup(name, is_background, id, exename,
138- mainscript,
139+ helper_app_paths = []
140+
141+ def do_setup(is_background, id, exename,
142 verbose=False,
143- doprepare=False):
144+ doprepare=False,
145+ is_main_app=False):
146 """Customize plist & options and run setup.
147
148+ - is_background controls showing a dock icon and menubar
149+ - id is the bundle id, in reverse dns style
150+ - exename is the name of the exe to be generated, usually
151+ should just be the basename of the main script without .py
152+
153+ - verbose - sent to py2app, controls spewage
154+ - doprepare - set to True only the first call of setup()
155+
156+ - is_main_app - if False, adds app path to list of helpers,
157+ which have their dependencies merged into the main app
158+ bundle in a post-processing step below.
159+
160 Returns the path of the generated .app bundle.
161 """
162+ name = APP_NAMES[exename]
163+ mainscript = os.path.join("bin", exename + ".py")
164
165 print "calling setup for:", name
166 plist = copy.deepcopy(master_plist)
167@@ -451,50 +504,47 @@
168 else:
169 prepare_class = DoNotPrepareAnything
170
171- setup(cmdclass=dict(prepare=prepare_class),
172+ setup(cmdclass=dict(prepare=prepare_class,
173+ clean=Clean),
174 app=[mainscript],
175 options=dict(py2app=options),
176 verbose=verbose)
177
178- return os.path.join("dist", exename, name + ".app")
179+ app_path = os.path.join("dist", exename, name + ".app")
180+ if not is_main_app:
181+ helper_app_paths.append(app_path)
182+ return app_path
183
184- sso_login_qt_app_path = do_setup("Ubuntu Single Sign-On",
185- True,
186+ sso_login_qt_app_path = do_setup(True,
187 "com.ubuntu.sso.login-qt",
188 "ubuntu-sso-login-qt",
189- os.path.join("bin",
190- "ubuntu-sso-login-qt.py"),
191 doprepare=True)
192
193- sso_login_app_path = do_setup("Ubuntu SSO Helper",
194- True,
195+ proxy_creds_app_path = do_setup(True,
196+ "com.ubuntu.sso.ssl-cert",
197+ "ubuntu-sso-proxy-creds-qt")
198+
199+ sso_login_app_path = do_setup(True,
200 "com.ubuntu.sso.login",
201- "ubuntu-sso-login",
202- os.path.join("bin", "ubuntu-sso-login.py"))
203+ "ubuntu-sso-login")
204
205- proxy_tunnel_app_path = do_setup("UbuntuOne Proxy Tunnel",
206- True,
207+ proxy_tunnel_app_path = do_setup(True,
208 "com.ubuntu.one.proxy-tunnel",
209- "ubuntuone-proxy-tunnel",
210- os.path.join("bin",
211- "ubuntuone-proxy-tunnel.py"))
212+ "ubuntuone-proxy-tunnel")
213
214- syncdaemon_app_path = do_setup("UbuntuOne Syncdaemon",
215- True,
216+ syncdaemon_app_path = do_setup(True,
217 "com.ubuntu.one.syncdaemon",
218- "ubuntuone-syncdaemon",
219- os.path.join("bin",
220- "ubuntuone-syncdaemon.py"))
221+ "ubuntuone-syncdaemon")
222
223- control_panel_script = os.path.join("bin", "ubuntuone-control-panel-qt.py")
224- control_panel_app_path = do_setup("UbuntuOne",
225- False,
226+ control_panel_app_path = do_setup(False,
227 "com.ubuntu.one.controlpanel",
228- "ubuntuone-control-panel",
229- control_panel_script)
230+ "ubuntuone-control-panel-qt",
231+ is_main_app=True)
232
233- control_panel_resources_path = os.path.join(control_panel_app_path,
234- "Contents", "Resources")
235+ control_panel_contents_path = os.path.join(control_panel_app_path,
236+ "Contents")
237+ control_panel_resources_path = os.path.join(control_panel_contents_path,
238+ "Resources")
239
240 # only do tweaks if you've done setup.py py2app
241 if 'py2app' not in sys.argv:
242@@ -505,60 +555,171 @@
243
244 print "Starting post-setup tweaks"
245
246- # move plugins around
247- frameworks_paths = [os.path.join(p, "Contents", "Frameworks") for p in
248- sso_login_qt_app_path,
249- sso_login_app_path,
250- control_panel_app_path]
251+ # move Qt plugins into Frameworks/plugins/subdir/
252+ control_panel_frameworks_path = os.path.join(control_panel_contents_path,
253+ "Frameworks")
254
255 plugin_names = [os.path.basename(f) for f in dylib_paths]
256
257- for f_path in frameworks_paths:
258- bearers_dir = os.path.join(f_path, "plugins", "bearer")
259- imagefmts_dir = os.path.join(f_path, "plugins", "imageformats")
260- try:
261- os.makedirs(imagefmts_dir)
262- except Exception as e:
263- print "ignoring exception in makedirs(%r): %r" % (imagefmts_dir,
264- e)
265-
266- try:
267- os.makedirs(bearers_dir)
268- except Exception as e:
269- print "ignoring exception in makedirs(%r): %r" % (bearers_dir, e)
270-
271- try:
272- for plugin_name in plugin_names:
273- if "bearer" in plugin_name:
274- print "moving ", os.path.join(f_path, plugin_name), "to"
275- print " ", os.path.join(bearers_dir, plugin_name)
276- os.rename(os.path.join(f_path, plugin_name),
277- os.path.join(bearers_dir, plugin_name))
278- else:
279- os.rename(os.path.join(f_path, plugin_name),
280- os.path.join(imagefmts_dir, plugin_name))
281- print "moving ", os.path.join(f_path, plugin_name), "to"
282- print " ", os.path.join(imagefmts_dir, plugin_name)
283- except Exception as e:
284- print "ERROR moving:", e
285-
286- print "copying helper apps into main app"
287-
288- def copy_helper(helper_path):
289- """Copy sub-apps into main app resources folder."""
290+ bearers_dir = os.path.join(control_panel_frameworks_path,
291+ "plugins", "bearer")
292+ imagefmts_dir = os.path.join(control_panel_frameworks_path,
293+ "plugins", "imageformats")
294+ try:
295+ os.makedirs(imagefmts_dir)
296+ except Exception as e:
297+ print "ignoring exception in makedirs(%r): %r" % (imagefmts_dir,
298+ e)
299+
300+ try:
301+ os.makedirs(bearers_dir)
302+ except Exception as e:
303+ print "ignoring exception in makedirs(%r): %r" % (bearers_dir, e)
304+
305+ try:
306+ for plugin_name in plugin_names:
307+ plugin_src = os.path.join(control_panel_frameworks_path,
308+ plugin_name)
309+
310+ if "bearer" in plugin_name:
311+ plugin_dest = os.path.join(bearers_dir, plugin_name)
312+ else:
313+ plugin_dest = os.path.join(imagefmts_dir, plugin_name)
314+
315+ os.rename(plugin_src, plugin_dest)
316+
317+ except Exception as e:
318+ print "WARNING, got exception moving plugins around:", e
319+
320+ def get_packages_zip_file(resources_path):
321+ """Get a ZipFile instance of the site_packages for an app."""
322+ zip_glob = os.path.join(resources_path,
323+ 'lib', 'python*',
324+ 'site-packages.zip')
325+ zip_paths = glob.glob(zip_glob)
326+ if len(zip_paths) != 1:
327+ raise Exception("zip_glob %r expanded to %r, wanted 1 path." %
328+ (zip_glob, zip_paths))
329+
330+ return ZipFile(zip_paths[0], 'a')
331+
332+ def get_dynload_files(resources_path):
333+ """Get a set of files in the lib-dynload directory."""
334+ file_set = set()
335+ dynload_glob = os.path.join(resources_path,
336+ 'lib', 'python*',
337+ 'lib-dynload')
338+ dynload_paths = glob.glob(dynload_glob)
339+ if len(dynload_paths) != 1:
340+ raise Exception("dynload_glob %r expanded to %r, wanted 1 path." %
341+ (dynload_glob, dynload_paths))
342+
343+ dynload_path = dynload_paths[0]
344+ for root, dirs, files in os.walk(dynload_path):
345+ for f in files:
346+ path = os.path.join(root, f)
347+ relpath = os.path.relpath(path, dynload_path)
348+ file_set.add(relpath)
349+ return dynload_path, file_set
350+
351+ main_zip = get_packages_zip_file(control_panel_resources_path)
352+ main_zip_files = set(main_zip.namelist())
353+
354+ (main_dynload_path,
355+ main_dynload_files) = get_dynload_files(control_panel_resources_path)
356+
357+ print "Copying helper apps into main app"
358+
359+ for helper_path in helper_app_paths:
360+
361+ print "Processing", helper_path
362
363 helper_app_name = os.path.basename(helper_path)
364- dest = os.path.join(control_panel_resources_path,
365+ helper_src_contents_path = os.path.join(helper_path,
366+ "Contents")
367+
368+ helper_dest = os.path.join(control_panel_resources_path,
369 helper_app_name)
370- if os.path.exists(dest):
371- print "removing", dest
372- shutil.rmtree(dest)
373-
374- shutil.copytree(helper_path, dest)
375-
376- copy_helper(sso_login_qt_app_path)
377- copy_helper(sso_login_app_path)
378- copy_helper(proxy_tunnel_app_path)
379- copy_helper(syncdaemon_app_path)
380+ if os.path.exists(helper_dest):
381+ print "%r exists. removing it." % helper_dest
382+ shutil.rmtree(helper_dest)
383+
384+ helper_dest_contents_path = os.path.join(helper_dest, "Contents")
385+
386+ os.makedirs(helper_dest_contents_path)
387+
388+ # copy actual Contents/MacOS and plist, symlink everything else
389+
390+ shutil.copytree(os.path.join(helper_src_contents_path, "MacOS"),
391+ os.path.join(helper_dest_contents_path, "MacOS"))
392+
393+ shutil.copy(os.path.join(helper_src_contents_path, "Info.plist"),
394+ os.path.join(helper_dest_contents_path, "Info.plist"))
395+
396+ # link to the rest of the items in the main app bundle, except:
397+ # - the .app bundles, to avoid circular links
398+ # __boot__.py needs to be the one from the subapp
399+
400+ resource_subdirs_to_link = ['include', 'lib']
401+
402+ helper_src_resources_path = os.path.join(helper_src_contents_path,
403+ "Resources")
404+
405+ helper_dest_resources_path = os.path.join(helper_dest_contents_path,
406+ "Resources")
407+ os.mkdir(helper_dest_resources_path)
408+
409+ helper_src_resources = os.listdir(helper_src_resources_path)
410+
411+ # copy everything in resources that isn't include, lib, or *.app:
412+ resources_to_copy = [p for p in helper_src_resources if
413+ p not in resource_subdirs_to_link]
414+
415+ for rsrc_name in resources_to_copy:
416+ shutil.copy(os.path.join(helper_src_resources_path, rsrc_name),
417+ os.path.join(helper_dest_resources_path, rsrc_name))
418+
419+ # link to resources/include and resources/lib from the main bundle:
420+ resources_to_link = [os.path.join("Resources", p)
421+ for p in resource_subdirs_to_link]
422+
423+ # also link Frameworks directory
424+ for src_item in ["Frameworks", "PkgInfo"] + resources_to_link:
425+ src_path = os.path.join(os.path.pardir,
426+ os.path.pardir,
427+ os.path.pardir,
428+ src_item)
429+ # add another pardir to make Resources/* items work:
430+ if src_item.startswith("Resources"):
431+ src_path = os.path.join(os.path.pardir, src_path)
432+
433+ link_name = os.path.join(helper_dest_contents_path, src_item)
434+ os.symlink(src_path, link_name)
435+
436+ print "Merging site-packages.zip files"
437+ helper_zip = get_packages_zip_file(helper_src_resources_path)
438+ helper_zip_files = set(helper_zip.namelist())
439+ add_from_helper = helper_zip_files - main_zip_files
440+
441+ for name in add_from_helper:
442+ main_zip.writestr(name, helper_zip.read(name))
443+ helper_zip.close()
444+
445+ print "Merging lib-dynload contents"
446+ (helper_dynload_path,
447+ helper_dynload_files) = get_dynload_files(helper_src_resources_path)
448+ add_from_helper = helper_dynload_files - main_dynload_files
449+
450+ for filename in add_from_helper:
451+ src = os.path.join(helper_dynload_path,
452+ filename)
453+ dest = os.path.join(main_dynload_path,
454+ filename)
455+ dest_dir = os.path.dirname(dest)
456+ if not os.path.exists(dest_dir):
457+ os.makedirs(dest_dir)
458+ shutil.copy(src, dest)
459+
460+ main_zip.close()
461
462 print "DONE. see dist/ for the .app."

Subscribers

People subscribed via source and target branches