diff options
Diffstat (limited to 'linden/indra/newview/viewer_manifest.py')
-rwxr-xr-x | linden/indra/newview/viewer_manifest.py | 450 |
1 files changed, 450 insertions, 0 deletions
diff --git a/linden/indra/newview/viewer_manifest.py b/linden/indra/newview/viewer_manifest.py new file mode 100755 index 0000000..4c422e4 --- /dev/null +++ b/linden/indra/newview/viewer_manifest.py | |||
@@ -0,0 +1,450 @@ | |||
1 | #!/usr/bin/python | ||
2 | # @file viewer_manifest.py | ||
3 | # @author Ryan Williams | ||
4 | # @brief Description of all installer viewer files, and methods for packaging | ||
5 | # them into installers for all supported platforms. | ||
6 | # | ||
7 | # Copyright (c) 2006-2007, Linden Research, Inc. | ||
8 | # | ||
9 | # The source code in this file ("Source Code") is provided by Linden Lab | ||
10 | # to you under the terms of the GNU General Public License, version 2.0 | ||
11 | # ("GPL"), unless you have obtained a separate licensing agreement | ||
12 | # ("Other License"), formally executed by you and Linden Lab. Terms of | ||
13 | # the GPL can be found in doc/GPL-license.txt in this distribution, or | ||
14 | # online at http://secondlife.com/developers/opensource/gplv2 | ||
15 | # | ||
16 | # There are special exceptions to the terms and conditions of the GPL as | ||
17 | # it is applied to this Source Code. View the full text of the exception | ||
18 | # in the file doc/FLOSS-exception.txt in this software distribution, or | ||
19 | # online at http://secondlife.com/developers/opensource/flossexception | ||
20 | # | ||
21 | # By copying, modifying or distributing this software, you acknowledge | ||
22 | # that you have read and understood your obligations described above, | ||
23 | # and agree to abide by those obligations. | ||
24 | # | ||
25 | # ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO | ||
26 | # WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, | ||
27 | # COMPLETENESS OR PERFORMANCE. | ||
28 | import sys | ||
29 | import os.path | ||
30 | import re | ||
31 | import tarfile | ||
32 | viewer_dir = os.path.dirname(__file__) | ||
33 | # add llmanifest library to our path so we don't have to muck with PYTHONPATH | ||
34 | sys.path.append(os.path.join(viewer_dir, '../lib/python/indra')) | ||
35 | from llmanifest import LLManifest, main, proper_windows_path, path_ancestors | ||
36 | |||
37 | class ViewerManifest(LLManifest): | ||
38 | def construct(self): | ||
39 | super(ViewerManifest, self).construct() | ||
40 | self.exclude("*.svn*") | ||
41 | self.path(src="../../scripts/messages/message_template.msg", dst="app_settings/message_template.msg") | ||
42 | |||
43 | if self.prefix(src="app_settings"): | ||
44 | self.exclude("logcontrol.xml") | ||
45 | self.exclude("logcontrol-dev.xml") | ||
46 | self.path("*.pem") | ||
47 | self.path("*.ini") | ||
48 | self.path("*.xml") | ||
49 | self.path("*.vp") | ||
50 | self.path("*.db2") | ||
51 | |||
52 | # include the entire shaders directory recursively | ||
53 | self.path("shaders") | ||
54 | self.end_prefix("app_settings") | ||
55 | |||
56 | if self.prefix(src="character"): | ||
57 | self.path("*.llm") | ||
58 | self.path("*.xml") | ||
59 | self.path("*.tga") | ||
60 | self.end_prefix("character") | ||
61 | |||
62 | |||
63 | # Include our fonts | ||
64 | if self.prefix(src="fonts"): | ||
65 | self.path("*.ttf") | ||
66 | self.path("*.txt") | ||
67 | self.end_prefix("fonts") | ||
68 | |||
69 | # XUI | ||
70 | if self.prefix(src="skins"): | ||
71 | # include the entire textures directory recursively | ||
72 | self.path("textures") | ||
73 | self.path("paths.xml") | ||
74 | self.path("xui/*/*.xml") | ||
75 | self.path('words.*.txt') | ||
76 | |||
77 | # Local HTML files (e.g. loading screen) | ||
78 | if self.prefix("html/*"): | ||
79 | self.path("*.html") | ||
80 | self.path("*.gif") | ||
81 | self.path("*.jpg") | ||
82 | self.path("*.css") | ||
83 | self.end_prefix("html/*") | ||
84 | self.end_prefix("skins") | ||
85 | |||
86 | self.path("featuretable.txt") | ||
87 | self.path("releasenotes.txt") | ||
88 | self.path("lsl_guide.html") | ||
89 | self.path("gpu_table.txt") | ||
90 | |||
91 | def flags_list(self): | ||
92 | """ Convenience function that returns the command-line flags for the grid""" | ||
93 | if(self.args['grid'] == ''): | ||
94 | return "" | ||
95 | elif(self.args['grid'] == 'firstlook'): | ||
96 | return '-settings settings_firstlook.xml' | ||
97 | else: | ||
98 | return ("-settings settings_beta.xml --%(grid)s -helperuri http://preview-%(grid)s.secondlife.com/helpers/" % {'grid':self.args['grid']}) | ||
99 | |||
100 | def login_url(self): | ||
101 | """ Convenience function that returns the appropriate login url for the grid""" | ||
102 | if(self.args.get('login_url')): | ||
103 | return self.args['login_url'] | ||
104 | else: | ||
105 | if(self.args['grid'] == ''): | ||
106 | return 'http://secondlife.com/app/login/' | ||
107 | elif(self.args['grid'] == 'firstlook'): | ||
108 | return 'http://secondlife.com/app/login/firstlook/' | ||
109 | else: | ||
110 | return 'http://secondlife.com/app/login/beta/' | ||
111 | |||
112 | def replace_login_url(self): | ||
113 | # set the login page to point to a url appropriate for the type of client | ||
114 | self.replace_in("skins/xui/en-us/panel_login.xml", searchdict={'http://secondlife.com/app/login/':self.login_url()}) | ||
115 | |||
116 | |||
117 | class WindowsManifest(ViewerManifest): | ||
118 | def final_exe(self): | ||
119 | # *NOTE: these are the only two executable names that the crash reporter recognizes | ||
120 | if self.args['grid'] == '': | ||
121 | return "SecondLife.exe" | ||
122 | elif self.args['grid'] == 'firstlook': | ||
123 | return "SecondLifeFirstLook.exe" | ||
124 | else: | ||
125 | return "SecondLifePreview.exe" | ||
126 | # return "SecondLifePreview%s.exe" % (self.args['grid'], ) | ||
127 | |||
128 | def construct(self): | ||
129 | super(WindowsManifest, self).construct() | ||
130 | # the final exe is complicated because we're not sure where it's coming from, | ||
131 | # nor do we have a fixed name for the executable | ||
132 | self.path(self.find_existing_file('ReleaseForDownload/Secondlife.exe', 'Secondlife.exe', 'ReleaseNoOpt/newview_noopt.exe'), dst=self.final_exe()) | ||
133 | # need to get the kdu dll from any of the build directories as well | ||
134 | self.path(self.find_existing_file('ReleaseForDownload/llkdu.dll', 'llkdu.dll', '../../libraries/i686-win32/lib_release/llkdu.dll'), dst='llkdu.dll') | ||
135 | self.path(src="licenses-win32.txt", dst="licenses.txt") | ||
136 | |||
137 | # For use in crash reporting (generates minidumps) | ||
138 | self.path("dbghelp.dll") | ||
139 | |||
140 | # For using FMOD for sound... DJS | ||
141 | self.path("fmod.dll") | ||
142 | |||
143 | # Mozilla appears to force a dependency on these files so we need to ship it (CP) | ||
144 | self.path("msvcr71.dll") | ||
145 | self.path("msvcp71.dll") | ||
146 | |||
147 | # Mozilla runtime DLLs (CP) | ||
148 | if self.prefix(src="../../libraries/i686-win32/lib_release", dst=""): | ||
149 | self.path("gksvggdiplus.dll") | ||
150 | self.path("js3250.dll") | ||
151 | self.path("nspr4.dll") | ||
152 | self.path("nss3.dll") | ||
153 | self.path("nssckbi.dll") | ||
154 | self.path("plc4.dll") | ||
155 | self.path("plds4.dll") | ||
156 | self.path("smime3.dll") | ||
157 | self.path("softokn3.dll") | ||
158 | self.path("ssl3.dll") | ||
159 | self.path("xpcom.dll") | ||
160 | self.path("xul.dll") | ||
161 | self.end_prefix() | ||
162 | |||
163 | # Mozilla runtime misc files (CP) | ||
164 | if self.prefix(src="app_settings/mozilla"): | ||
165 | self.path("chrome/*.*") | ||
166 | self.path("components/*.*") | ||
167 | self.path("greprefs/*.*") | ||
168 | self.path("plugins/*.*") | ||
169 | self.path("res/*.*") | ||
170 | self.path("res/*/*") | ||
171 | self.end_prefix() | ||
172 | |||
173 | # # pull in the crash logger and updater from other projects | ||
174 | # self.path(src="../win_crash_logger/win_crash_logger.exe", dst="win_crash_logger.exe") | ||
175 | self.path(src="../win_updater/updater.exe", dst="updater.exe") | ||
176 | self.replace_login_url() | ||
177 | |||
178 | def nsi_file_commands(self, install=True): | ||
179 | def wpath(path): | ||
180 | if(path.endswith('/') or path.endswith(os.path.sep)): | ||
181 | path = path[:-1] | ||
182 | path = path.replace('/', '\\') | ||
183 | return path | ||
184 | |||
185 | result = "" | ||
186 | dest_files = [pair[1] for pair in self.file_list if pair[0] and os.path.isfile(pair[1])] | ||
187 | # sort deepest hierarchy first | ||
188 | dest_files.sort(lambda a,b: cmp(a.count(os.path.sep),b.count(os.path.sep)) or cmp(a,b)) | ||
189 | dest_files.reverse() | ||
190 | out_path = None | ||
191 | for pkg_file in dest_files: | ||
192 | rel_file = os.path.normpath(pkg_file.replace(self.get_dst_prefix()+os.path.sep,'')) | ||
193 | installed_dir = wpath(os.path.join('$INSTDIR', os.path.dirname(rel_file))) | ||
194 | pkg_file = wpath(os.path.normpath(pkg_file)) | ||
195 | if installed_dir != out_path: | ||
196 | if(install): | ||
197 | out_path = installed_dir | ||
198 | result += 'SetOutPath ' + out_path + '\n' | ||
199 | if(install): | ||
200 | result += 'File ' + pkg_file + '\n' | ||
201 | else: | ||
202 | result += 'Delete ' + wpath(os.path.join('$INSTDIR', rel_file)) + '\n' | ||
203 | # at the end of a delete, just rmdir all the directories | ||
204 | if(not install): | ||
205 | deleted_file_dirs = [os.path.dirname(pair[1].replace(self.get_dst_prefix()+os.path.sep,'')) for pair in self.file_list] | ||
206 | # find all ancestors so that we don't skip any dirs that happened to have no non-dir children | ||
207 | deleted_dirs = [] | ||
208 | for d in deleted_file_dirs: | ||
209 | deleted_dirs.extend(path_ancestors(d)) | ||
210 | # sort deepest hierarchy first | ||
211 | deleted_dirs.sort(lambda a,b: cmp(a.count(os.path.sep),b.count(os.path.sep)) or cmp(a,b)) | ||
212 | deleted_dirs.reverse() | ||
213 | prev = None | ||
214 | for d in deleted_dirs: | ||
215 | if d != prev: # skip duplicates | ||
216 | result += 'RMDir ' + wpath(os.path.join('$INSTDIR', os.path.normpath(d))) + '\n' | ||
217 | prev = d | ||
218 | |||
219 | return result | ||
220 | |||
221 | def package_finish(self): | ||
222 | version_vars_template = """ | ||
223 | !define INSTEXE "%(final_exe)s" | ||
224 | !define VERSION "%(version_short)s" | ||
225 | !define VERSION_LONG "%(version)s" | ||
226 | !define VERSION_DASHES "%(version_dashes)s" | ||
227 | """ | ||
228 | if(self.args['grid'] == ''): | ||
229 | installer_file = "Second Life %(version_dashes)s Setup.exe" | ||
230 | grid_vars_template = """ | ||
231 | OutFile "%(outfile)s" | ||
232 | !define INSTFLAGS "%(flags)s" | ||
233 | !define INSTNAME "SecondLife" | ||
234 | !define SHORTCUT "Second Life" | ||
235 | !define URLNAME "secondlife" | ||
236 | Caption "Second Life ${VERSION}" | ||
237 | """ | ||
238 | else: | ||
239 | installer_file = "Second Life %(version_dashes)s (%(grid_caps)s) Setup.exe" | ||
240 | grid_vars_template = """ | ||
241 | OutFile "%(outfile)s" | ||
242 | !define INSTFLAGS "%(flags)s" | ||
243 | !define INSTNAME "SecondLife%(grid_caps)s" | ||
244 | !define SHORTCUT "Second Life (%(grid_caps)s)" | ||
245 | !define URLNAME "secondlife%(grid)s" | ||
246 | !define UNINSTALL_SETTINGS 1 | ||
247 | Caption "Second Life %(grid)s ${VERSION}" | ||
248 | """ | ||
249 | if(self.args.has_key('installer_name')): | ||
250 | installer_file = self.args['installer_name'] | ||
251 | else: | ||
252 | installer_file = installer_file % {'version_dashes' : '-'.join(self.args['version']), | ||
253 | 'grid_caps' : self.args['grid'].upper()} | ||
254 | tempfile = "../secondlife_setup.nsi" | ||
255 | # the following is an odd sort of double-string replacement | ||
256 | self.replace_in("installers/windows/installer_template.nsi", tempfile, { | ||
257 | "%%VERSION%%":version_vars_template%{'version_short' : '.'.join(self.args['version'][:-1]), | ||
258 | 'version' : '.'.join(self.args['version']), | ||
259 | 'version_dashes' : '-'.join(self.args['version']), | ||
260 | 'final_exe' : self.final_exe()}, | ||
261 | "%%GRID_VARS%%":grid_vars_template%{'grid':self.args['grid'], | ||
262 | 'grid_caps':self.args['grid'].upper(), | ||
263 | 'outfile':installer_file, | ||
264 | 'flags':self.flags_list()}, | ||
265 | "%%INSTALL_FILES%%":self.nsi_file_commands(True), | ||
266 | "%%DELETE_FILES%%":self.nsi_file_commands(False)}) | ||
267 | |||
268 | NSIS_path = 'C:\\Program Files\\NSIS\\makensis.exe' | ||
269 | self.run_command('"' + proper_windows_path(NSIS_path) + '" ' + self.dst_path_of(tempfile)) | ||
270 | self.remove(self.dst_path_of(tempfile)) | ||
271 | self.created_path(installer_file) | ||
272 | |||
273 | |||
274 | class DarwinManifest(ViewerManifest): | ||
275 | def construct(self): | ||
276 | # copy over the build result (this is a no-op if run within the xcode script) | ||
277 | self.path("build/" + self.args['configuration'] + "/Second Life.app", dst="") | ||
278 | |||
279 | if self.prefix(src="", dst="Contents"): # everything goes in Contents | ||
280 | # Expand the tar file containing the assorted mozilla bits into | ||
281 | # <bundle>/Contents/MacOS/ | ||
282 | self.contents_of_tar('mozilla-universal-darwin.tgz', 'MacOS') | ||
283 | |||
284 | # replace the default theme with our custom theme (so scrollbars work). | ||
285 | if self.prefix(src="mozilla-theme", dst="MacOS/chrome"): | ||
286 | self.path("classic.jar") | ||
287 | self.path("classic.manifest") | ||
288 | self.end_prefix("MacOS/chrome") | ||
289 | |||
290 | # most everything goes in the Resources directory | ||
291 | if self.prefix(src="", dst="Resources"): | ||
292 | super(DarwinManifest, self).construct() | ||
293 | |||
294 | # the trial directory seems to be not used [rdw] | ||
295 | self.path('trial') | ||
296 | |||
297 | if self.prefix("cursors_mac"): | ||
298 | self.path("*.tif") | ||
299 | self.end_prefix("cursors_mac") | ||
300 | |||
301 | self.path("licenses-mac.txt", dst="licenses.txt") | ||
302 | self.path("featuretable_mac.txt") | ||
303 | self.path("secondlife.icns") | ||
304 | |||
305 | # llkdu dynamic library | ||
306 | # self.path("../../libraries/universal-darwin/lib_release/libllkdu.dylib", "libllkdu.dylib") | ||
307 | |||
308 | # command line arguments for connecting to the proper grid | ||
309 | self.put_in_file(self.flags_list(), 'arguments.txt') | ||
310 | |||
311 | # set the proper login url | ||
312 | self.replace_login_url() | ||
313 | |||
314 | self.end_prefix("Resources") | ||
315 | |||
316 | self.end_prefix("Contents") | ||
317 | |||
318 | # NOTE: the -S argument to strip causes it to keep enough info for | ||
319 | # annotated backtraces (i.e. function names in the crash log). 'strip' with no | ||
320 | # arguments yields a slightly smaller binary but makes crash logs mostly useless. | ||
321 | # This may be desirable for the final release. Or not. | ||
322 | if("package" in self.args['actions'] or | ||
323 | "unpacked" in self.args['actions']): | ||
324 | self.run_command('strip -S "%(viewer_binary)s"' % | ||
325 | { 'viewer_binary' : self.dst_path_of('Contents/MacOS/Second Life')}) | ||
326 | |||
327 | |||
328 | def package_finish(self): | ||
329 | imagename="SecondLife_" + '_'.join(self.args['version']) | ||
330 | if(self.args['grid'] != ''): | ||
331 | imagename = imagename + '_' + self.args['grid'].upper() | ||
332 | |||
333 | sparsename = imagename + ".sparseimage" | ||
334 | finalname = imagename + ".dmg" | ||
335 | # make sure we don't have stale files laying about | ||
336 | self.remove(sparsename, finalname) | ||
337 | |||
338 | self.run_command('hdiutil create "%(sparse)s" -volname "Second Life" -fs HFS+ -type SPARSE -megabytes 300' % {'sparse':sparsename}) | ||
339 | |||
340 | # mount the image and get the name of the mount point and device node | ||
341 | hdi_output = self.run_command('hdiutil attach -private "' + sparsename + '"') | ||
342 | devfile = re.search("/dev/disk([0-9]+)[^s]", hdi_output).group(0).strip() | ||
343 | volpath = re.search('HFS\s+(.+)', hdi_output).group(1).strip() | ||
344 | |||
345 | # Copy everything in to the mounted .dmg | ||
346 | # TODO change name of .app once mac_updater can handle it. | ||
347 | for s,d in { | ||
348 | self.get_dst_prefix():"Second Life.app", | ||
349 | "lsl_guide.html":"Linden Scripting Language Guide.html", | ||
350 | "releasenotes.txt":"Release Notes.txt", | ||
351 | "installers/darwin/mac_image_hidden":".hidden", | ||
352 | "installers/darwin/mac_image_background.tga":"background.tga", | ||
353 | "installers/darwin/mac_image_DS_Store":".DS_Store"}.items(): | ||
354 | |||
355 | print "Copying to dmg", s, d | ||
356 | self.copy_action(self.src_path_of(s), os.path.join(volpath, d)) | ||
357 | |||
358 | # Unmount the image | ||
359 | self.run_command('hdiutil detach "' + devfile + '"') | ||
360 | |||
361 | print "Converting temp disk image to final disk image" | ||
362 | self.run_command('hdiutil convert "%(sparse)s" -format UDZO -imagekey zlib-level=9 -o "%(final)s"' % {'sparse':sparsename, 'final':finalname}) | ||
363 | # get rid of the temp file | ||
364 | self.remove(sparsename) | ||
365 | |||
366 | class LinuxManifest(ViewerManifest): | ||
367 | def construct(self): | ||
368 | super(LinuxManifest, self).construct() | ||
369 | self.path("licenses-linux.txt","licenses.txt") | ||
370 | self.path("res/ll_icon.ico","secondlife.ico") | ||
371 | if self.prefix("linux_tools", ""): | ||
372 | self.path("client-readme.txt","README-linux.txt") | ||
373 | self.path("wrapper.sh","secondlife") | ||
374 | self.path("unicode.ttf","unicode.ttf") | ||
375 | self.end_prefix("linux_tools") | ||
376 | |||
377 | # Create an appropriate gridargs.dat for this package, denoting required grid. | ||
378 | self.put_in_file(self.flags_list(), 'gridargs.dat') | ||
379 | # set proper login url | ||
380 | self.replace_login_url() | ||
381 | |||
382 | # stripping all the libs removes a few megabytes from the end-user package | ||
383 | for s,d in self.file_list: | ||
384 | if re.search("lib/lib.+\.so.*", d): | ||
385 | self.run_command('strip -S %s' % d) | ||
386 | |||
387 | |||
388 | def package_finish(self): | ||
389 | if(self.args.has_key('installer_name')): | ||
390 | installer_name = self.args['installer_name'] | ||
391 | else: | ||
392 | installer_name = '_'.join('SecondLife_', self.args.get('arch'), *self.args['version']) | ||
393 | if grid != '': | ||
394 | installer_name += "_" + grid.upper() | ||
395 | |||
396 | # temporarily move directory tree so that it has the right name in the tarfile | ||
397 | self.run_command("mv %(dst)s %(inst)s" % {'dst':self.get_dst_prefix(),'inst':self.src_path_of(installer_name)}) | ||
398 | # --numeric-owner hides the username of the builder for security etc. | ||
399 | self.run_command('tar -C %(dir)s --numeric-owner -cjf %(inst_path)s.tar.bz2 %(inst_name)s' % {'dir':self.get_src_prefix(), 'inst_name': installer_name, 'inst_path':self.src_path_of(installer_name)}) | ||
400 | self.run_command("mv %(inst)s %(dst)s" % {'dst':self.get_dst_prefix(),'inst':self.src_path_of(installer_name)}) | ||
401 | |||
402 | class Linux_i686Manifest(LinuxManifest): | ||
403 | def construct(self): | ||
404 | super(Linux_i686Manifest, self).construct() | ||
405 | self.path("secondlife-i686-bin-stripped","bin/do-not-directly-run-secondlife-bin") | ||
406 | # self.path("../linux_crash_logger/linux-crash-logger-i686-bin-stripped","linux-crash-logger.bin") | ||
407 | self.path("linux_tools/launch_url.sh","launch_url.sh") | ||
408 | if self.prefix("res-sdl"): | ||
409 | self.path("*") | ||
410 | # recurse | ||
411 | self.end_prefix("res-sdl") | ||
412 | |||
413 | self.path("app_settings/mozilla-runtime-linux-i686") | ||
414 | |||
415 | if self.prefix("../../libraries/i686-linux/lib_release_client", "lib"): | ||
416 | # self.path("libkdu_v42R.so") | ||
417 | self.path("libfmod-3.75.so") | ||
418 | self.path("libapr-1.so.0") | ||
419 | self.path("libaprutil-1.so.0") | ||
420 | self.path("libdb-4.2.so") | ||
421 | self.path("libogg.so.0") | ||
422 | self.path("libvorbis.so.0") | ||
423 | self.path("libvorbisfile.so.0") | ||
424 | self.path("libvorbisenc.so.0") | ||
425 | self.path("libcurl.so.4") | ||
426 | self.path("libcrypto.so.0.9.7") | ||
427 | self.path("libssl.so.0.9.7") | ||
428 | self.path("libexpat.so.1") | ||
429 | # self.path("libstdc++.so.6") | ||
430 | self.path("libelfio.so") | ||
431 | self.path("libuuid.so", "libuuid.so.1") | ||
432 | self.path("libSDL-1.2.so.0") | ||
433 | # self.path("libllkdu.so", "../bin/libllkdu.so") # llkdu goes in bin for some reason | ||
434 | self.end_prefix("lib") | ||
435 | |||
436 | |||
437 | class Linux_x86_64Manifest(LinuxManifest): | ||
438 | def construct(self): | ||
439 | super(Linux_x86_64Manifest, self).construct() | ||
440 | self.path("secondlife-x86_64-bin-stripped","bin/secondlife-bin") | ||
441 | # TODO: I get the sense that this isn't fully fleshed out | ||
442 | if self.prefix("../../libraries/x86_64-linux/lib_release_client", "lib"): | ||
443 | # self.path("libkdu_v42R.so") | ||
444 | self.path("libxmlrpc.so.0") | ||
445 | # # self.path("libllkdu.so", "../bin/libllkdu.so") # llkdu goes in bin for some reason | ||
446 | self.end_prefix("lib") | ||
447 | |||
448 | |||
449 | if __name__ == "__main__": | ||
450 | main(srctree=viewer_dir, dsttree=os.path.join(viewer_dir, "packaged")) | ||