|
1 #!/usr/bin/env python |
|
2 # -*- coding: utf-8 -*- |
|
3 |
|
4 ############################################################################## |
|
5 # cmd_py2sis.py - Ensymble command line tool, py2sis command |
|
6 # Copyright 2006, 2007, 2008, 2009 Jussi Ylänen |
|
7 # |
|
8 # Portions Copyright (c) 2008-2009 Nokia Corporation |
|
9 # |
|
10 # This file is part of Ensymble developer utilities for Symbian OS(TM). |
|
11 # |
|
12 # Ensymble is free software; you can redistribute it and/or modify |
|
13 # it under the terms of the GNU General Public License as published by |
|
14 # the Free Software Foundation; either version 2 of the License, or |
|
15 # (at your option) any later version. |
|
16 # |
|
17 # Ensymble is distributed in the hope that it will be useful, |
|
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
20 # GNU General Public License for more details. |
|
21 # |
|
22 # You should have received a copy of the GNU General Public License |
|
23 # along with Ensymble; if not, write to the Free Software |
|
24 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
|
25 ############################################################################## |
|
26 |
|
27 import sys |
|
28 import os |
|
29 import re |
|
30 import imp |
|
31 import getopt |
|
32 import getpass |
|
33 import locale |
|
34 import zlib |
|
35 import shutil |
|
36 import time |
|
37 import zipfile |
|
38 |
|
39 import sisfile |
|
40 import sisfield |
|
41 import symbianutil |
|
42 import rscfile |
|
43 import miffile |
|
44 import module_repo |
|
45 import py_compile |
|
46 |
|
47 |
|
48 ############################################################################## |
|
49 # Specify the magic number of the Python interpreter used in PyS60. |
|
50 ############################################################################## |
|
51 pys60_magic_number = '\xb3\xf2\r\n' |
|
52 |
|
53 |
|
54 ############################################################################## |
|
55 # Help texts |
|
56 ############################################################################## |
|
57 |
|
58 shorthelp = 'Create a SIS package for a "Python for S60" application' |
|
59 longhelp = '''py2sis |
|
60 [--uid=0x01234567] [--appname=AppName] [--version=1.0.0] |
|
61 [--lang=EN,...] [--icon=icon.svg] [--shortcaption="App. Name",...] |
|
62 [--caption="Application Name",...] [--drive=C] [--extrasdir=root] |
|
63 [--textfile=mytext_%C.txt] [--cert=mycert.cer] [--privkey=mykey.key] |
|
64 [--passphrase=12345] [--heapsize=min,max] [--caps=Cap1+Cap2+...] |
|
65 [--vendor="Vendor Name",...] [--autostart] [--runinstall] |
|
66 [--encoding=terminal,filesystem] [--verbose] [--profile=s60ui] |
|
67 [--mode=pys60] [--extra-modules=mod1,...] [--sourcecode] |
|
68 [--ignore-missing-deps] [--platform-uid=0x101f7961,...] |
|
69 <src> [sisfile] |
|
70 |
|
71 Create a SIS package for a "Python for S60" application. |
|
72 |
|
73 Options: |
|
74 src - Source script or directory |
|
75 sisfile - Path of the created SIS file |
|
76 uid - Symbian OS UID for the application |
|
77 appname - Name of the application |
|
78 version - Application version: X.Y.Z or X,Y,Z (major, minor, build) |
|
79 lang - Comma separated list of two-character language codes |
|
80 icon - Icon file in SVG-Tiny format |
|
81 shortcaption - Comma separated list of short captions in all languages |
|
82 caption - Comma separated list of long captions in all languages |
|
83 drive - Drive where the package will be installed (any by default) |
|
84 extrasdir - Name of dir. tree placed under drive root (none by default) |
|
85 textfile - Text file (or pattern, see below) to display during install |
|
86 cert - Certificate to use for signing (PEM format) |
|
87 privkey - Private key of the certificate (PEM format) |
|
88 passphrase - Pass phrase of the private key (insecure, use stdin instead) |
|
89 caps - Capability names, separated by "+" (LocalServices, NetworkServices, |
|
90 ReadUserData, WriteUserData and UserEnvironment by default) |
|
91 vendor - Vendor name or a comma separated list of names in all lang. |
|
92 autostart - Application is registered to start on each device boot |
|
93 runinstall - Application is automatically started after installation |
|
94 heapsize - Application heap size, min. and/or max. ("100K,4M" by default) |
|
95 encoding - Local character encodings for terminal and filesystem |
|
96 verbose - Print extra statistics |
|
97 profile - Script execution environment. Either console or s60ui |
|
98 mode - Module to be packaged when names conflict. Either from pycore or pys60 |
|
99 extra-modules - Additional dependency modules that should be packaged with the application |
|
100 sourcecode - Package scripts as source code. By default the scripts are packaged as bytecode |
|
101 ignore-missing-deps - Ignore any missing dependencies during packaging |
|
102 platform-uid - UID of the supported S60 platforms |
|
103 |
|
104 If no certificate and its private key are given, a default self-signed |
|
105 certificate is used to sign the SIS file. Software authors are encouraged |
|
106 to create their own unique certificates for SIS packages that are to be |
|
107 distributed. |
|
108 |
|
109 If no icon is given, the Python logo is used as the icon. The Python |
|
110 logo is a trademark of the Python Software Foundation. |
|
111 |
|
112 Text to display uses UTF-8 encoding. The file name may contain formatting |
|
113 characters that are substituted for each selected language. If no formatting |
|
114 characters are present, the same text will be used for all languages. |
|
115 |
|
116 %% - literal % |
|
117 %n - language number (01 - 99) |
|
118 %c - two-character language code in lowercase letters |
|
119 %C - two-character language code in capital letters |
|
120 %l - language name in English, using only lowercase letters |
|
121 %l - language name in English, using mixed case letters |
|
122 ''' |
|
123 |
|
124 |
|
125 ############################################################################## |
|
126 # Parameters |
|
127 ############################################################################## |
|
128 |
|
129 MAXPASSPHRASELENGTH = 256 |
|
130 MAXCERTIFICATELENGTH = 65536 |
|
131 MAXPRIVATEKEYLENGTH = 65536 |
|
132 MAXICONFILESIZE = 65536 |
|
133 MAXOTHERFILESIZE = 1024 * 1024 * 8 # Eight megabytes |
|
134 MAXTEXTFILELENGTH = 1024 |
|
135 |
|
136 |
|
137 ############################################################################## |
|
138 # Public module-level functions |
|
139 ############################################################################## |
|
140 |
|
141 |
|
142 def run(pgmname, argv): |
|
143 # Determine system character encodings. |
|
144 try: |
|
145 # getdefaultlocale() may sometimes return None. |
|
146 # Fall back to ASCII encoding in that case. |
|
147 terminalenc = locale.getdefaultlocale()[1] + "" |
|
148 except TypeError: |
|
149 # Invalid locale, fall back to ASCII terminal encoding. |
|
150 terminalenc = "ascii" |
|
151 |
|
152 try: |
|
153 # sys.getfilesystemencoding() was introduced in Python v2.3 and |
|
154 # it can sometimes return None. Fall back to ASCII if something |
|
155 # goes wrong. |
|
156 filesystemenc = sys.getfilesystemencoding() + "" |
|
157 except (AttributeError, TypeError): |
|
158 filesystemenc = "ascii" |
|
159 |
|
160 try: |
|
161 gopt = getopt.gnu_getopt |
|
162 except: |
|
163 # Python <v2.3, GNU-style parameter ordering not supported. |
|
164 gopt = getopt.getopt |
|
165 |
|
166 # Parse command line arguments. |
|
167 short_opts = "u:n:r:l:i:s:c:f:x:t:a:k:p:b:d:gRH:e:vhP:m:o:Sy:Iq" |
|
168 long_opts = [ |
|
169 "uid=", "appname=", "version=", "lang=", "icon=", |
|
170 "shortcaption=", "caption=", "drive=", "extrasdir=", "textfile=", |
|
171 "cert=", "privkey=", "passphrase=", "caps=", "vendor=", |
|
172 "autostart", "runinstall", "heapsize=", |
|
173 "encoding=", "verbose", "debug", "help", "profile=", "mode=", |
|
174 "extra-modules=", "sourcecode", "ignore-missing-deps", "platform-uid="] |
|
175 args = gopt(argv, short_opts, long_opts) |
|
176 |
|
177 opts = dict(args[0]) |
|
178 pargs = args[1] |
|
179 |
|
180 if len(pargs) == 0: |
|
181 raise ValueError("no source file name given") |
|
182 |
|
183 # Override character encoding of command line and filesystem. |
|
184 encs = opts.get("--encoding", opts.get("-e", "%s,%s" % (terminalenc, |
|
185 filesystemenc))) |
|
186 try: |
|
187 terminalenc, filesystemenc = encs.split(",") |
|
188 except (ValueError, TypeError): |
|
189 raise ValueError("invalid encoding string '%s'" % encs) |
|
190 |
|
191 # Determine if debug output is requested. |
|
192 if "--debug" in opts.keys(): |
|
193 # Enable debug output for OpenSSL-related functions. |
|
194 import cryptutil |
|
195 cryptutil.setdebug(True) |
|
196 module_repo.debug = True |
|
197 |
|
198 module_repo.debug_log = open("debug.txt", "w") |
|
199 |
|
200 # Check if ignore-missing-deps flag is set. If not then abort sis |
|
201 # generation printing the missing dependencies as errors. |
|
202 if "--ignore-missing-deps" in opts.keys() or "-I" in opts.keys(): |
|
203 module_repo.ignore_missing_deps = True |
|
204 |
|
205 # If sourcecode option is set then the application is packaged as source |
|
206 # code. By default it is packaged as bytecode. |
|
207 bytecode = True |
|
208 if "--sourcecode" in opts.keys() or "-S" in opts.keys(): |
|
209 bytecode = False |
|
210 |
|
211 # Get source name, either a Python program or a directory. |
|
212 src = pargs[0].decode(terminalenc).encode(filesystemenc) |
|
213 |
|
214 extrasdir = opts.get("--extrasdir", opts.get("-x", None)) |
|
215 if extrasdir != None: |
|
216 extrasdir = extrasdir.decode(terminalenc).encode(filesystemenc) |
|
217 if extrasdir[-1] == os.sep: |
|
218 # Strip trailing slash (or backslash). |
|
219 extrasdir = extrasdir[:-1] |
|
220 |
|
221 if os.sep in extrasdir: |
|
222 raise ValueError("%s: too many path components" % extrasdir) |
|
223 if not os.path.isdir(src): |
|
224 raise RuntimeError("extrasdir option can only be used when the" + |
|
225 " source is a directory") |
|
226 if not os.path.isdir(os.path.join(os.path.abspath(src), extrasdir)): |
|
227 raise RuntimeError("Directory specified using --extrasdir" +\ |
|
228 " option does not exist") |
|
229 |
|
230 # Get the profile in which the python script should be executed - OpenC |
|
231 # console mode or s60ui mode |
|
232 profile = opts.get("--profile", opts.get("-P", "s60ui")) |
|
233 if profile not in ['console', 's60ui']: |
|
234 raise ValueError("Invalid profile. Set either console or s60ui") |
|
235 |
|
236 # Check if Python core modules take priority over PyS60 modules |
|
237 mode = opts.get("--mode", opts.get("-m", "pycore")) |
|
238 if mode not in ['pycore', 'pys60']: |
|
239 raise ValueError("Invalid mode. Set either pys60 or pycore") |
|
240 |
|
241 # Get the S60 platform uid of the devices on which the Python |
|
242 # application is supported |
|
243 platform_uid = opts.get("--platform-uid", "0x101f7961,0x1028315F") |
|
244 |
|
245 # Extra module(s) to be packaged with application sis |
|
246 extra_modules = opts.get("--extra-modules", opts.get("-o", None)) |
|
247 |
|
248 # module_repo.get_dependency_list returns a map with the following format: |
|
249 # {'repo': [<dependency list of standard repo modules>], |
|
250 # 'dev': [<dependency list of dev modules>]} |
|
251 extra_modules_map = module_repo.get_dependency_list(src, extra_modules) |
|
252 |
|
253 if mode == 'pys60': |
|
254 # Add calendar.py and socket.py wrappers to app's private directory |
|
255 conflicting_modules = {'calendar': 'e32calendar', 'socket': 'btsocket'} |
|
256 |
|
257 for module in conflicting_modules: |
|
258 if module in extra_modules_map[module_repo.std_repo_module]: |
|
259 extra_modules_map[module_repo.std_repo_module].remove(module) |
|
260 extra_modules_map[module_repo.dev_repo_module].append( |
|
261 conflicting_modules[module]) |
|
262 |
|
263 tmpdir_suffix = '_' + time.strftime("%H%M%S") |
|
264 |
|
265 if not os.path.isdir(src): |
|
266 # app is a file. Create app directory, rename app file to |
|
267 # default.py, and add dependency |
|
268 srcdir, srcfile = os.path.split(src) |
|
269 appdir = os.path.join(srcdir, srcfile[:-3] + tmpdir_suffix) |
|
270 os.mkdir(appdir) |
|
271 shutil.copy(src, appdir) |
|
272 os.rename(os.path.join(appdir, srcfile), |
|
273 os.path.join(appdir, "default.py")) |
|
274 src = appdir |
|
275 else: |
|
276 module_repo.debug_print("Copying '%s' to '%s' " % |
|
277 (os.path.abspath(src), os.path.abspath(src) + tmpdir_suffix)) |
|
278 shutil.copytree(os.path.abspath(src), |
|
279 os.path.abspath(src) + tmpdir_suffix) |
|
280 src += tmpdir_suffix |
|
281 appdir = src |
|
282 # appdir should be deleted if there is any exception during packaging |
|
283 try: |
|
284 if extra_modules_map: |
|
285 module_repo.debug_print("Extra modules list:" + str(extra_modules_map)) |
|
286 module_repo.appdir = appdir |
|
287 module_repo.extrasdir = extrasdir |
|
288 dep_module_paths = module_repo.search_module(extra_modules_map) |
|
289 if module_repo.error_count != 0: |
|
290 raise RuntimeError(str(module_repo.error_count) +\ |
|
291 " dependency files not found") |
|
292 module_repo.process_dependent_modules(dep_module_paths) |
|
293 |
|
294 extrasdir = module_repo.extrasdir |
|
295 |
|
296 if os.path.isdir(src): |
|
297 # Add calendar.py and socket.py wrappers to app's lib.zip |
|
298 conflicting_modules = {'calendar_py': 'calendar.py', |
|
299 'socket_py': 'socket.py'} |
|
300 if mode == 'pys60': |
|
301 for module in conflicting_modules: |
|
302 module_repo.debug_print("Adding socket & calendar " + |
|
303 "wrappers") |
|
304 shutil.copy( |
|
305 os.path.join("templates", conflicting_modules[module]), |
|
306 os.path.join(src, conflicting_modules[module])) |
|
307 # All the py[c|o] files in the app directory except for default.py |
|
308 # are zipped into lib.zip. This file is added to sys.path by |
|
309 # launcher.py |
|
310 py_ignore_list = [] |
|
311 lib_zip = zipfile.ZipFile(os.path.join(src, |
|
312 "lib.zip"), "w", zipfile.ZIP_DEFLATED) |
|
313 |
|
314 def process_bytecode(dirpath, name, file_ext): |
|
315 # Process the files to be written to the lib_zip based on the |
|
316 # `bytecode` option |
|
317 if not bytecode or file_ext in ['.pyc', '.pyo']: |
|
318 final_filename = name + file_ext |
|
319 elif bytecode and file_ext == '.py': |
|
320 if imp.get_magic() != pys60_magic_number: |
|
321 raise RuntimeError("Python Version on \ |
|
322 the host system not bytecode compatible with \ |
|
323 the Python version used in PyS60") |
|
324 py_compile.compile(os.path.join(dirpath, name + file_ext)) |
|
325 # Compilation can result in either a pyc or a pyo. |
|
326 for ext in ['.pyc', '.pyo']: |
|
327 if os.path.exists(os.path.join(dirpath, name + ext)): |
|
328 final_filename = name + ext |
|
329 |
|
330 return final_filename |
|
331 |
|
332 for dirpath, dirs, files in os.walk(src): |
|
333 if extrasdir in dirs: |
|
334 dirs.remove(extrasdir) |
|
335 for filename in files: |
|
336 name, file_ext = os.path.splitext(os.path.basename( |
|
337 filename)) |
|
338 file_ext = file_ext.lower() |
|
339 if file_ext == '.py': |
|
340 for ext in ['.pyc', '.pyo']: |
|
341 if os.path.exists(os.path.join(dirpath, |
|
342 name + ext)): |
|
343 os.remove(os.path.join(dirpath, name + ext)) |
|
344 files.remove(name + ext) |
|
345 |
|
346 if file_ext in ['.py', '.pyc', '.pyo'] and \ |
|
347 filename != 'default.py': |
|
348 final_filename = process_bytecode(dirpath, name, |
|
349 file_ext) |
|
350 relative_path = \ |
|
351 os.path.join(dirpath, final_filename).split( |
|
352 os.path.basename(src) + os.sep)[-1] |
|
353 module_repo.debug_print("Adding %s to lib.zip as %s" %\ |
|
354 (os.path.join(src, relative_path), relative_path)) |
|
355 lib_zip.write(os.path.join(src, relative_path), |
|
356 relative_path) |
|
357 if extra_modules_map: |
|
358 # After copying the files to the lib_zip delete |
|
359 # them otherwise they too will find their way into |
|
360 # the zip. |
|
361 os.remove(os.path.join(src, relative_path)) |
|
362 if bytecode and file_ext == '.py': |
|
363 # The `py` files which will be compiled will |
|
364 # also have to be deleted. |
|
365 os.remove(os.path.join(dirpath, filename)) |
|
366 else: |
|
367 py_ignore_list.append(relative_path) |
|
368 |
|
369 lib_zip.close() |
|
370 if not len(lib_zip.infolist()): |
|
371 # Delete the lib_zip if it is empty |
|
372 os.remove(os.path.join(src, "lib.zip")) |
|
373 |
|
374 # Remove trailing slashes (or whatever the separator is). |
|
375 src = os.path.split(src + os.sep)[0] |
|
376 |
|
377 # Use last directory component as the name. |
|
378 basename = os.path.basename(src) |
|
379 |
|
380 # Source is a directory, recursively collect files it contains. |
|
381 srcdir = src |
|
382 srcfiles = [] |
|
383 prefixlen = len(srcdir) + len(os.sep) |
|
384 def getfiles(arg, dirname, names): |
|
385 for name in names: |
|
386 path = os.path.join(dirname, name) |
|
387 if not os.path.isdir(path) and \ |
|
388 path[prefixlen:] not in py_ignore_list: |
|
389 arg.append(path[prefixlen:]) |
|
390 os.path.walk(srcdir, getfiles, srcfiles) |
|
391 |
|
392 # Read application version and UID3 from default.py. |
|
393 version, uid3 = scandefaults(os.path.join(srcdir, "default.py")) |
|
394 else: |
|
395 if src.lower().endswith(".py"): |
|
396 # Use program name without the .py extension. |
|
397 basename = os.path.basename(src)[:-3] |
|
398 else: |
|
399 # Unknown extension, use program name as-is. |
|
400 basename = os.path.basename(src) |
|
401 |
|
402 # Source is a file, use it. |
|
403 srcdir, srcfiles = os.path.split(src) |
|
404 srcfiles = [srcfiles] |
|
405 |
|
406 # Read application version and UID3 from file. |
|
407 version, uid3 = scandefaults(os.path.join(srcdir, srcfiles[0])) |
|
408 |
|
409 # The sis file name should not have the tmpdir_suffix appended to src |
|
410 basename = basename.rsplit(tmpdir_suffix, 1)[0] |
|
411 |
|
412 # Parse version string, use 1.0.0 by default. |
|
413 version = opts.get("--version", opts.get("-r", version)) |
|
414 if version == None: |
|
415 version = "1.0.0" |
|
416 print ("%s: warning: no application version given, " |
|
417 "using %s" % (pgmname, version)) |
|
418 try: |
|
419 version = parseversion(version) |
|
420 except (ValueError, IndexError, TypeError): |
|
421 raise ValueError("invalid version string '%s'" % version) |
|
422 |
|
423 # Determine output SIS file name. |
|
424 if len(pargs) == 1: |
|
425 # Derive output file name from input file name. |
|
426 outfile = "%s_v%d_%d_%d.sis" % (basename, version[0], |
|
427 version[1], version[2]) |
|
428 elif len(pargs) == 2: |
|
429 outfile = pargs[1].decode(terminalenc).encode(filesystemenc) |
|
430 if os.path.isdir(outfile): |
|
431 # Output to directory, derive output name from input file name. |
|
432 outfile = os.path.join(outfile, "%s_v%d_%d_%d.sis" % ( |
|
433 basename, version[0], version[1], version[2])) |
|
434 if not outfile.lower().endswith(".sis"): |
|
435 outfile += ".sis" |
|
436 else: |
|
437 raise ValueError("wrong number of arguments") |
|
438 |
|
439 # Determine application name (install dir.), use basename by default. |
|
440 appname = opts.get("--appname", opts.get("-n", basename)) |
|
441 appname = appname.decode(terminalenc) |
|
442 |
|
443 # Auto-generate a test-range UID from application name. |
|
444 autouid = symbianutil.uidfromname(appname) |
|
445 |
|
446 # Get UID3. |
|
447 uid3 = opts.get("--uid", opts.get("-u", uid3)) |
|
448 if uid3 == None: |
|
449 # No UID given, use auto-generated UID. |
|
450 uid3 = autouid |
|
451 print ("%s: warning: no UID given, using auto-generated " |
|
452 "test-range UID 0x%08x" % (pgmname, uid3)) |
|
453 elif uid3.lower().startswith("0x"): |
|
454 # Prefer hex UIDs with leading "0x". |
|
455 uid3 = long(uid3, 16) |
|
456 else: |
|
457 try: |
|
458 if len(uid3) == 8: |
|
459 # Assuming hex UID even without leading "0x". |
|
460 print ('%s: warning: assuming hex UID even ' |
|
461 'without leading "0x"' % pgmname) |
|
462 uid3 = long(uid3, 16) |
|
463 else: |
|
464 # Decimal UID. |
|
465 uid3 = long(uid3) |
|
466 print ('%s: warning: decimal UID converted to 0x%08x' % |
|
467 (pgmname, uid3)) |
|
468 except ValueError: |
|
469 raise ValueError("invalid UID string '%s'" % uid3) |
|
470 |
|
471 # Warn against specifying a test-range UID manually. |
|
472 if uid3 & 0xf0000000L == 0xe0000000L and uid3 != autouid: |
|
473 print ("%s: warning: manually specifying a test-range UID is " |
|
474 "not recommended" % pgmname) |
|
475 |
|
476 # Determine application language(s), use "EN" by default. |
|
477 lang = opts.get("--lang", opts.get("-l", "EN")).split(",") |
|
478 numlang = len(lang) |
|
479 |
|
480 # Verify that the language codes are correct. |
|
481 for l in lang: |
|
482 try: |
|
483 symbianutil.langidtonum[l] |
|
484 except KeyError: |
|
485 raise ValueError("%s: no such language code" % l) |
|
486 |
|
487 # Get icon file name. |
|
488 icon = opts.get("--icon", opts.get("-i", None)) |
|
489 if icon != None: |
|
490 icon = icon.decode(terminalenc).encode(filesystemenc) |
|
491 |
|
492 # Read icon file. |
|
493 f = file(icon, "rb") |
|
494 icondata = f.read(MAXICONFILESIZE + 1) |
|
495 f.close() |
|
496 |
|
497 if len(icondata) > MAXICONFILESIZE: |
|
498 raise ValueError("icon file too large") |
|
499 else: |
|
500 # No icon given, use a default icon. |
|
501 icondata = zlib.decompress(defaulticondata.decode("base-64")) |
|
502 |
|
503 # Determine application short caption(s). |
|
504 shortcaption = opts.get("--shortcaption", opts.get("-s", "")) |
|
505 shortcaption = shortcaption.decode(terminalenc) |
|
506 if len(shortcaption) == 0: |
|
507 # Short caption not given, use application name. |
|
508 shortcaption = [appname] * numlang |
|
509 else: |
|
510 shortcaption = shortcaption.split(",") |
|
511 |
|
512 # Determine application long caption(s), use short caption by default. |
|
513 caption = opts.get("--caption", opts.get("-c", "")) |
|
514 caption = caption.decode(terminalenc) |
|
515 if len(caption) == 0: |
|
516 # Caption not given, use short caption. |
|
517 caption = shortcaption |
|
518 else: |
|
519 caption = caption.split(",") |
|
520 |
|
521 # Compare the number of languages and captions. |
|
522 if len(shortcaption) != numlang or len(caption) != numlang: |
|
523 raise ValueError("invalid number of captions") |
|
524 |
|
525 # Determine installation drive, any by default. |
|
526 drive = opts.get("--drive", opts.get("-f", "any")).upper() |
|
527 if drive == "ANY" or drive == "!": |
|
528 drive = "!" |
|
529 elif drive != "C" and drive != "E": |
|
530 raise ValueError("%s: invalid drive letter" % drive) |
|
531 |
|
532 # Determine vendor name(s), use "Ensymble" by default. |
|
533 vendor = opts.get("--vendor", opts.get("-d", "Ensymble")) |
|
534 vendor = vendor.decode(terminalenc) |
|
535 vendor = vendor.split(",") |
|
536 if len(vendor) == 1: |
|
537 # Only one vendor name given, use it for all languages. |
|
538 vendor = vendor * numlang |
|
539 elif len(vendor) != numlang: |
|
540 raise ValueError("invalid number of vendor names") |
|
541 |
|
542 # Load text files. |
|
543 texts = [] |
|
544 textfile = opts.get("--textfile", opts.get("-t", None)) |
|
545 if textfile != None: |
|
546 texts = readtextfiles(textfile, lang) |
|
547 |
|
548 # Get certificate and its private key file names. |
|
549 cert = opts.get("--cert", opts.get("-a", None)) |
|
550 privkey = opts.get("--privkey", opts.get("-k", None)) |
|
551 if cert != None and privkey != None: |
|
552 # Convert file names from terminal encoding to filesystem encoding. |
|
553 cert = cert.decode(terminalenc).encode(filesystemenc) |
|
554 privkey = privkey.decode(terminalenc).encode(filesystemenc) |
|
555 |
|
556 # Read certificate file. |
|
557 f = file(cert, "rb") |
|
558 certdata = f.read(MAXCERTIFICATELENGTH + 1) |
|
559 f.close() |
|
560 |
|
561 if len(certdata) > MAXCERTIFICATELENGTH: |
|
562 raise ValueError("certificate file too large") |
|
563 |
|
564 # Read private key file. |
|
565 f = file(privkey, "rb") |
|
566 privkeydata = f.read(MAXPRIVATEKEYLENGTH + 1) |
|
567 f.close() |
|
568 |
|
569 if len(privkeydata) > MAXPRIVATEKEYLENGTH: |
|
570 raise ValueError("private key file too large") |
|
571 elif cert == None and privkey == None: |
|
572 # No certificate given, use the Ensymble default certificate. |
|
573 # defaultcert.py is not imported when not needed. This speeds |
|
574 # up program start-up a little. |
|
575 import defaultcert |
|
576 certdata = defaultcert.cert |
|
577 privkeydata = defaultcert.privkey |
|
578 |
|
579 print ("%s: warning: no certificate given, using " |
|
580 "insecure built-in one" % pgmname) |
|
581 |
|
582 # Warn if the UID is in the protected range. |
|
583 # Resulting SIS file will probably not install. |
|
584 if uid3 < 0x80000000L: |
|
585 print ("%s: warning: UID is in the protected range " |
|
586 "(0x00000000 - 0x7ffffff)" % pgmname) |
|
587 else: |
|
588 raise ValueError("missing certificate or private key") |
|
589 |
|
590 # Get pass phrase. Pass phrase remains in terminal encoding. |
|
591 passphrase = opts.get("--passphrase", opts.get("-p", None)) |
|
592 if passphrase == None and privkey != None: |
|
593 # Private key given without "--passphrase" option, ask it. |
|
594 if sys.stdin.isatty(): |
|
595 # Standard input is a TTY, ask password interactively. |
|
596 passphrase = getpass.getpass("Enter private key pass phrase:") |
|
597 else: |
|
598 # Not connected to a TTY, read stdin non-interactively instead. |
|
599 passphrase = sys.stdin.read(MAXPASSPHRASELENGTH + 1) |
|
600 |
|
601 if len(passphrase) > MAXPASSPHRASELENGTH: |
|
602 raise ValueError("pass phrase too long") |
|
603 |
|
604 passphrase = passphrase.strip() |
|
605 |
|
606 # Get capabilities and normalize the names. |
|
607 caps = opts.get("--caps", opts.get("-b", |
|
608 "LocalServices+NetworkServices+ReadUserData+WriteUserData+" + |
|
609 "UserEnvironment")) |
|
610 capmask = symbianutil.capstringtomask(caps) |
|
611 caps = symbianutil.capmasktostring(capmask, True) |
|
612 |
|
613 # Determine if the application is requested to start on each device |
|
614 # boot. |
|
615 autostart = False |
|
616 if "--autostart" in opts.keys() or "-g" in opts.keys(): |
|
617 autostart = True |
|
618 |
|
619 runinstall = False |
|
620 if "--runinstall" in opts.keys() or "-R" in opts.keys(): |
|
621 runinstall = True |
|
622 |
|
623 # Get heap sizes. |
|
624 heapsize = \ |
|
625 opts.get("--heapsize", opts.get("-H", "100K,4M")).split(",", 1) |
|
626 try: |
|
627 heapsizemin = symbianutil.parseintmagnitude(heapsize[0]) |
|
628 if len(heapsize) == 1: |
|
629 # Only one size given, use it as both. |
|
630 heapsizemax = heapsizemin |
|
631 else: |
|
632 heapsizemax = symbianutil.parseintmagnitude(heapsize[1]) |
|
633 except (ValueError, TypeError, IndexError): |
|
634 raise ValueError( |
|
635 "%s: invalid heap size, one or two values expected" % |
|
636 ",".join(heapsize)) |
|
637 |
|
638 # Warn if the minimum heap size is larger than the maximum heap size. |
|
639 # Resulting SIS file will probably not install. |
|
640 if heapsizemin > heapsizemax: |
|
641 print ("%s: warning: minimum heap size larger than " |
|
642 "maximum heap size" % pgmname) |
|
643 |
|
644 # Determine verbosity. |
|
645 verbose = False |
|
646 if "--verbose" in opts.keys() or "-v" in opts.keys(): |
|
647 verbose = True |
|
648 |
|
649 |
|
650 # Ingredients for successful SIS generation: |
|
651 # |
|
652 # terminalenc Terminal character encoding (autodetected) |
|
653 # filesystemenc File system name encoding (autodetected) |
|
654 # basename Base for generated file names on host, filesystemenc encoded |
|
655 # srcdir Directory of source files, filesystemenc encoded |
|
656 # srcfiles List of filesystemenc encoded source file names in srcdir |
|
657 # outfile Output SIS file name, filesystemenc encoded |
|
658 # uid3 Application UID3, long integer |
|
659 # appname Application name and install directory in device, in Unicode |
|
660 # version A triple-item tuple (major, minor, build) |
|
661 # lang List of two-character language codes, ASCII strings |
|
662 # icon Icon data, a binary string typically containing a SVG-T file |
|
663 # shortcaption List of Unicode short captions, one per language |
|
664 # caption List of Unicode long captions, one per language |
|
665 # drive Installation drive letter or "!" |
|
666 # extrasdir Path prefix for extra files, filesystemenc encoded or None |
|
667 # textfile File name pattern of text file(s) to display during install |
|
668 # texts Actual texts to display during install, one per language |
|
669 # cert Certificate in PEM format |
|
670 # privkey Certificate private key in PEM format |
|
671 # passphrase Pass phrase of private key, terminalenc encoded string |
|
672 # caps, capmask Capability names and bitmask |
|
673 # vendor List of Unicode vendor names, one per language |
|
674 # autostart Boolean requesting application autostart on device boot |
|
675 # runinstall Boolean requesting application autorun after installation |
|
676 # heapsizemin Heap that must be available for the application to start |
|
677 # heapsizemax Maximum amount of heap the application can allocate |
|
678 # verbose Boolean indicating verbose terminal output |
|
679 # profile console/s60ui stub exe to be packaged with script |
|
680 # mode Selects the module to be packaged either from Python core |
|
681 # or PyS60 |
|
682 # extra-modules Additional dependency modules that should be packaged with |
|
683 # the application |
|
684 |
|
685 if verbose: |
|
686 print |
|
687 print "Input file(s) %s" % " ".join( |
|
688 [s.decode(filesystemenc).encode(terminalenc) for s in srcfiles]) |
|
689 print "Output SIS file %s" % ( |
|
690 outfile.decode(filesystemenc).encode(terminalenc)) |
|
691 print "UID 0x%08x" % uid3 |
|
692 print "Application name %s" % appname.encode(terminalenc) |
|
693 print "Version %d.%d.%d" % ( |
|
694 version[0], version[1], version[2]) |
|
695 print "Language(s) %s" % ", ".join(lang) |
|
696 print "Icon %s" % ((icon and |
|
697 icon.decode(filesystemenc).encode(terminalenc)) or "<default>") |
|
698 print "Short caption(s) %s" % ", ".join( |
|
699 [s.encode(terminalenc) for s in shortcaption]) |
|
700 print "Long caption(s) %s" % ", ".join( |
|
701 [s.encode(terminalenc) for s in caption]) |
|
702 print "Install drive %s" % ((drive == "!") and |
|
703 "<any>" or drive) |
|
704 print "Extras directory %s" % ((extrasdir and |
|
705 extrasdir.decode(filesystemenc).encode(terminalenc)) or "<none>") |
|
706 print "Text file(s) %s" % ((textfile and |
|
707 textfile.decode(filesystemenc).encode(terminalenc)) or "<none>") |
|
708 print "Certificate %s" % ((cert and |
|
709 cert.decode(filesystemenc).encode(terminalenc)) or "<default>") |
|
710 print "Private key %s" % ((privkey and |
|
711 privkey.decode(filesystemenc).encode(terminalenc)) or "<default>") |
|
712 print "Capabilities 0x%x (%s)" % (capmask, caps) |
|
713 print "Vendor name(s) %s" % ", ".join( |
|
714 [s.encode(terminalenc) for s in vendor]) |
|
715 print "Autostart on boot %s" % ((autostart and "Yes") or "No") |
|
716 print "Run after install %s" % ((runinstall and "Yes") or "No") |
|
717 print "Heap size in bytes %d, %d" % (heapsizemin, heapsizemax) |
|
718 print "Profile %s" % profile |
|
719 print "Mode %s" % mode |
|
720 print "Extra Modules %s" % extra_modules_map |
|
721 print |
|
722 |
|
723 # Generate SimpleSISWriter object. |
|
724 sw = sisfile.SimpleSISWriter(lang, caption, uid3, version, |
|
725 vendor[0], vendor) |
|
726 |
|
727 # Add text file or files to the SIS object. Text dialog is |
|
728 # supposed to be displayed before anything else is installed. |
|
729 if len(texts) == 1: |
|
730 sw.addfile(texts[0], operation = sisfield.EOpText) |
|
731 elif len(texts) > 1: |
|
732 sw.addlangdepfile(texts, operation = sisfield.EOpText) |
|
733 |
|
734 # Generate "Python for S60" resource file. |
|
735 rsctarget = u"%s:\\resource\\apps\\%s_0x%08x.rsc" % (drive, appname, uid3) |
|
736 string = zlib.decompress(pythons60rscdata.decode("base-64")) |
|
737 sw.addfile(string, rsctarget) |
|
738 del string |
|
739 |
|
740 # Generate application registration resource file. |
|
741 regtarget = u"%s:\\private\\10003a3f\\import\\apps\\%s_0x%08x_reg.rsc" % ( |
|
742 drive, appname, uid3) |
|
743 exename = u"%s_0x%08x" % (appname, uid3) |
|
744 locpath = u"\\resource\\apps\\%s_0x%08x_loc" % (appname, uid3) |
|
745 rw = rscfile.RSCWriter(uid2 = 0x101f8021, uid3 = uid3) |
|
746 # STRUCT APP_REGISTRATION_INFO from appinfo.rh |
|
747 res = rscfile.Resource(["LONG", "LLINK", "LTEXT", "LONG", "LTEXT", "LONG", |
|
748 "BYTE", "BYTE", "BYTE", "BYTE", "LTEXT", "BYTE", |
|
749 "WORD", "WORD", "WORD", "LLINK"], |
|
750 0, 0, exename, 0, locpath, 1, |
|
751 0, 0, 0, 0, "", 0, |
|
752 0, 0, 0, 0) |
|
753 rw.addresource(res) |
|
754 string = rw.tostring() |
|
755 del rw |
|
756 sw.addfile(string, regtarget) |
|
757 del string |
|
758 |
|
759 # EXE target name |
|
760 exetarget = u"%s:\\sys\\bin\\%s_0x%08x.exe" % (drive, appname, uid3) |
|
761 |
|
762 # Generate autostart registration resource file, if requested. |
|
763 if autostart: |
|
764 autotarget = u"%s:\\private\\101f875a\\import\\[%08x].rsc" % ( |
|
765 drive, uid3) |
|
766 rw = rscfile.RSCWriter(uid2 = 0, offset = " ") |
|
767 # STRUCT STARTUP_ITEM_INFO from startupitem.rh |
|
768 res = rscfile.Resource(["BYTE", "LTEXT", "WORD", |
|
769 "LONG", "BYTE", "BYTE"], |
|
770 0, exetarget, 0, 0, 0, 0) |
|
771 rw.addresource(res) |
|
772 string = rw.tostring() |
|
773 del rw |
|
774 sw.addfile(string, autotarget) |
|
775 del string |
|
776 |
|
777 # Generate localisable icon/caption definition resource files. |
|
778 iconpath = "\\resource\\apps\\%s_0x%08x_aif.mif" % (appname, uid3) |
|
779 for n in xrange(numlang): |
|
780 loctarget = u"%s:\\resource\\apps\\%s_0x%08x_loc.r%02d" % ( |
|
781 drive, appname, uid3, symbianutil.langidtonum[lang[n]]) |
|
782 rw = rscfile.RSCWriter(uid2 = 0, offset = " ") |
|
783 # STRUCT LOCALISABLE_APP_INFO from appinfo.rh |
|
784 res = rscfile.Resource(["LONG", "LLINK", "LTEXT", |
|
785 "LONG", "LLINK", "LTEXT", |
|
786 "WORD", "LTEXT", "WORD", "LTEXT"], |
|
787 0, 0, shortcaption[n], |
|
788 0, 0, caption[n], |
|
789 1, iconpath, 0, "") |
|
790 rw.addresource(res) |
|
791 string = rw.tostring() |
|
792 del rw |
|
793 sw.addfile(string, loctarget) |
|
794 del string |
|
795 |
|
796 # Generate MIF file for icon. |
|
797 icontarget = "%s:\\resource\\apps\\%s_0x%08x_aif.mif" % ( |
|
798 drive, appname, uid3) |
|
799 mw = miffile.MIFWriter() |
|
800 mw.addfile(icondata) |
|
801 del icondata |
|
802 string = mw.tostring() |
|
803 del mw |
|
804 sw.addfile(string, icontarget) |
|
805 del string |
|
806 if profile == 's60ui': |
|
807 target = "launcher.py" |
|
808 string = open(os.path.join("templates", target), "r").read() |
|
809 sw.addfile(string, "%s:\\private\\%08x\\%s" % (drive, uid3, target)) |
|
810 del string |
|
811 |
|
812 # Add files to SIS object. |
|
813 if len(srcfiles) == 1: |
|
814 # Read file. |
|
815 f = file(os.path.join(srcdir, srcfiles[0]), "rb") |
|
816 string = f.read(MAXOTHERFILESIZE + 1) |
|
817 f.close() |
|
818 |
|
819 if len(string) > MAXOTHERFILESIZE: |
|
820 raise ValueError("%s: input file too large" % srcfiles[0]) |
|
821 |
|
822 # Add file to the SIS object. One file only, rename it to default.py. |
|
823 target = "default.py" |
|
824 sw.addfile(string, "%s:\\private\\%08x\\%s" % (drive, uid3, target)) |
|
825 del string |
|
826 else: |
|
827 if extrasdir != None: |
|
828 sysbinprefix = os.path.join(extrasdir, "sys", "bin", "") |
|
829 else: |
|
830 sysbinprefix = os.path.join(os.sep, "sys", "bin", "") |
|
831 |
|
832 white_list = {} |
|
833 # More than one file, use original path names. |
|
834 for srcfile in srcfiles: |
|
835 # Read file. |
|
836 f = file(os.path.join(srcdir, srcfile), "rb") |
|
837 string = f.read(MAXOTHERFILESIZE + 1) |
|
838 f.close() |
|
839 |
|
840 if len(string) > MAXOTHERFILESIZE: |
|
841 raise ValueError("%s: input file too large" % srcfile) |
|
842 |
|
843 # Split path into components. |
|
844 srcpathcomp = srcfile.split(os.sep) |
|
845 |
|
846 # Check if the file is an E32Image (EXE or DLL). |
|
847 filecapmask = symbianutil.e32imagecaps(string) |
|
848 |
|
849 # Warn against common mistakes when dealing with E32Image files. |
|
850 if filecapmask != None: |
|
851 if not srcfile.startswith(sysbinprefix): |
|
852 # Warn against E32Image files outside /sys/bin. |
|
853 print ("%s: warning: %s is an E32Image (EXE or DLL) " |
|
854 "outside %s" % (pgmname, srcfile, sysbinprefix)) |
|
855 if filecapmask != capmask: |
|
856 # Warn capas of dll not equal to exe. Warn before |
|
857 # usind the exe capas |
|
858 module_repo.debug_print("%s: warning: Capas of %s is not " |
|
859 "equal to that of the exe. Capas of the exe will be" |
|
860 " assigned to this pyd" % (pgmname, srcfile)) |
|
861 string = symbianutil.e32imagecrc(string, |
|
862 capabilities=capmask) |
|
863 filecapmask = capmask |
|
864 |
|
865 srcfile_name = srcpathcomp.pop() |
|
866 try: |
|
867 file_name, file_ext = srcfile_name.split(".") |
|
868 except: |
|
869 file_ext = "" |
|
870 # Modify the file name if the file is a pyd and update the |
|
871 # white list with the new name. |
|
872 if srcfile.startswith(sysbinprefix) and file_ext == "pyd": |
|
873 srcfile_name = "%s_%08x.%s" % (file_name, uid3, file_ext) |
|
874 if not srcfile_name.startswith("${{PREFIX}}"): |
|
875 raise RuntimeError( |
|
876 'PYD is not prefixed with "${{PREFIX}}"') |
|
877 module_name = file_name.split("${{PREFIX}}")[1] |
|
878 white_list[module_name] = srcfile_name.split(".pyd")[0] |
|
879 srcpathcomp.append(srcfile_name) |
|
880 |
|
881 targetpathcomp = [s.decode(filesystemenc) for s in srcpathcomp] |
|
882 |
|
883 # Handle the extras directory. |
|
884 if extrasdir != None and extrasdir == srcpathcomp[0]: |
|
885 # Path is rooted at the drive root. |
|
886 targetfile = \ |
|
887 u"%s:\\%s" % (drive, "\\".join(targetpathcomp[1:])) |
|
888 else: |
|
889 # Path is rooted at the application private directory. |
|
890 targetfile = u"%s:\\private\\%08x\\%s" % ( |
|
891 drive, uid3, "\\".join(targetpathcomp)) |
|
892 |
|
893 # Add file to the SIS object. |
|
894 sw.addfile(string, targetfile, capabilities = filecapmask) |
|
895 del string |
|
896 |
|
897 # Write the white list to a file and package it |
|
898 if white_list != {}: |
|
899 whitelist_file = os.path.join(srcdir, "white-list.cfg") |
|
900 whitelist_f = open(whitelist_file, "wt") |
|
901 whitelist_f.write(repr(white_list)) |
|
902 whitelist_f.close() |
|
903 |
|
904 string = open(whitelist_file, "rb").read() |
|
905 sw.addfile(string, "%s:\\private\\%08x\\%s" % \ |
|
906 (drive, uid3, os.path.basename(whitelist_file))) |
|
907 del string |
|
908 os.remove(whitelist_file) |
|
909 |
|
910 ${{if INCLUDE_INTERNAL_SRC |
|
911 # Package iad client dll |
|
912 string = \ |
|
913 open(os.path.join("templates", "Py_iad_client.dll"), "rb").read() |
|
914 string = symbianutil.e32imagecrc(string, capabilities=capmask) |
|
915 sw.addfile( |
|
916 string, "%s:\\sys\\bin\\${{PREFIX}}Py_iad_client_0x%08x.dll" % \ |
|
917 (drive, uid3), None, capabilities = capmask) |
|
918 del string |
|
919 }} |
|
920 |
|
921 # Add target device dependency. |
|
922 platform_uids = platform_uid.split(",") |
|
923 for platform_uid in platform_uids: |
|
924 sw.addtargetdevice(eval(platform_uid), (0, 0, 0), None, |
|
925 ["Series60ProductID"] * numlang) |
|
926 |
|
927 # Add Python runtime as dependency |
|
928 sw.adddependency(${{PYS60_UID_CORE}}L, (${{PYS60_VERSION_MAJOR}}, |
|
929 ${{PYS60_VERSION_MINOR}}, ${{PYS60_VERSION_MICRO}}), |
|
930 None, ["Python runtime"] * numlang) |
|
931 |
|
932 # Add certificate. |
|
933 sw.addcertificate(privkeydata, certdata, passphrase) |
|
934 |
|
935 # Generate an EXE stub and add it to the SIS object. |
|
936 string = "" |
|
937 if profile == 's60ui': |
|
938 string = \ |
|
939 open(os.path.join("templates", "python_ui.exe"), "rb").read() |
|
940 else: |
|
941 string = open(os.path.join("templates", "python_console.exe"), |
|
942 "rb").read() |
|
943 |
|
944 string = symbianutil.e32imagecrc(string, uid3, uid3, None, |
|
945 heapsizemin, heapsizemax, capmask) |
|
946 |
|
947 if runinstall: |
|
948 # To avoid running without dependencies, this has to be in the end. |
|
949 sw.addfile(string, exetarget, None, capabilities = capmask, |
|
950 operation = sisfield.EOpRun, |
|
951 options = sisfield.EInstFileRunOptionInstall) |
|
952 else: |
|
953 sw.addfile(string, exetarget, None, capabilities = capmask) |
|
954 |
|
955 del string |
|
956 |
|
957 # Generate SIS file out of the SimpleSISWriter object. |
|
958 sw.tofile(outfile) |
|
959 except: |
|
960 raise |
|
961 finally: |
|
962 if os.path.exists(appdir): |
|
963 shutil.rmtree(appdir) |
|
964 |
|
965 if os.path.exists(os.path.join(src, "lib.zip")): |
|
966 os.remove(os.path.join(src, "lib.zip")) |
|
967 module_repo.debug_log.close() |
|
968 |
|
969 ############################################################################## |
|
970 # Module-level functions which are normally only used by this module |
|
971 ############################################################################## |
|
972 |
|
973 def scandefaults(filename): |
|
974 '''Scan a Python source file for application version string and UID3.''' |
|
975 |
|
976 version = None |
|
977 uid3 = None |
|
978 |
|
979 # Regular expression for the version string. Version may optionally |
|
980 # be enclosed in double or single quotes. |
|
981 version_ro = re.compile(r'SIS_VERSION\s*=\s*(?:(?:"([^"]*)")|' |
|
982 r"(?:'([^']*)')|(\S+))") |
|
983 |
|
984 # Original py2is uses a regular expression |
|
985 # r"SYMBIAN_UID\s*=\s*(0x[0-9a-fA-F]{8})". |
|
986 # This version is a bit more lenient. |
|
987 uid3_ro = re.compile(r"SYMBIAN_UID\s*=\s*(\S+)") |
|
988 |
|
989 # First match of each regular expression is used. |
|
990 f = file(filename, "rb") |
|
991 try: |
|
992 while version == None or uid3 == None: |
|
993 line = f.readline() |
|
994 if line == "": |
|
995 break |
|
996 if version == None: |
|
997 mo = version_ro.search(line) |
|
998 if mo: |
|
999 # Get first group that matched in the regular expression. |
|
1000 version = filter(None, mo.groups())[0] |
|
1001 if uid3 == None: |
|
1002 mo = uid3_ro.search(line) |
|
1003 if mo: |
|
1004 uid3 = mo.group(1) |
|
1005 finally: |
|
1006 f.close() |
|
1007 return version, uid3 |
|
1008 |
|
1009 def parseversion(version): |
|
1010 '''Parse a version string: "v1.2.3" or similar. |
|
1011 |
|
1012 Initial "v" can optionally be a capital "V" or omitted altogether. Minor |
|
1013 and build numbers can also be omitted. Separator can be a comma or a |
|
1014 period.''' |
|
1015 |
|
1016 version = version.strip().lower() |
|
1017 |
|
1018 # Strip initial "v" or "V". |
|
1019 if version[0] == "v": |
|
1020 version = version[1:] |
|
1021 |
|
1022 if "." in version: |
|
1023 parts = [int(n) for n in version.split(".")] |
|
1024 else: |
|
1025 parts = [int(n) for n in version.split(",")] |
|
1026 |
|
1027 # Allow missing minor and build numbers. |
|
1028 parts.extend([0, 0]) |
|
1029 |
|
1030 return parts[0:3] |
|
1031 |
|
1032 def readtextfiles(pattern, languages): |
|
1033 '''Read language dependent text files. |
|
1034 |
|
1035 Files are assumed to be in UTF-8 encoding and re-encoded |
|
1036 in UCS-2 (UTF-16LE) for Symbian OS to display during installation.''' |
|
1037 |
|
1038 if "%" not in pattern: |
|
1039 # Only one file, read it. |
|
1040 filenames = [pattern] |
|
1041 else: |
|
1042 filenames = [] |
|
1043 for langid in languages: |
|
1044 langnum = symbianutil.langidtonum[langid] |
|
1045 langname = symbianutil.langnumtoname[langnum] |
|
1046 |
|
1047 # Replace formatting characters in file name pattern. |
|
1048 filename = pattern |
|
1049 filename = filename.replace("%n", "%02d" % langnum) |
|
1050 filename = filename.replace("%c", langid.lower()) |
|
1051 filename = filename.replace("%C", langid.upper()) |
|
1052 filename = filename.replace("%l", langname.lower()) |
|
1053 filename = filename.replace("%L", langname) |
|
1054 filename = filename.replace("%%", "%") |
|
1055 |
|
1056 filenames.append(filename) |
|
1057 |
|
1058 texts = [] |
|
1059 |
|
1060 for filename in filenames: |
|
1061 f = file(filename, "r") # Read as text. |
|
1062 text = f.read(MAXTEXTFILELENGTH + 1) |
|
1063 f.close() |
|
1064 |
|
1065 if len(text) > MAXTEXTFILELENGTH: |
|
1066 raise ValueError("%s: text file too large" % filename) |
|
1067 |
|
1068 texts.append(text.decode("UTF-8").encode("UTF-16LE")) |
|
1069 |
|
1070 return texts |
|
1071 |
|
1072 ############################################################################## |
|
1073 # Embedded data: Icon and application resource |
|
1074 ############################################################################## |
|
1075 |
|
1076 # Python logo as a base-64-encoded, zlib-compressed SVG XML data |
|
1077 defaulticondata = ''' |
|
1078 eJyFVF1v20YQfC/Q/3Bl0be74+3tfQZRA1h24gJJa6COij66EiMSdSVDUiW3vz5zRyp2CgMVLHrJ |
|
1079 /ZjZmRNfv3n8614cu91+2G5mDWnTiG6z3K6GzXrWfLx9q1Lz5sdvv3n93eUv89vfb67E/rgWNx8v |
|
1080 3v80F41q29943raXt5fi18U7QZrE7bD5p22vfm5E0x8OD6/a9nQ66RPr7W7dvtvdPfTDct+iukV1 |
|
1081 6WwxkUgd0KdXh1VT0ArIH3f77ma3/TTcd7OmZJvnPKkRYL7Zv9qD6wO+szPc+YHeb//eLbtPwO30 |
|
1082 pjuMWFNSmYo1zpi9wNQaYwqzM8zj/bD586VCyjm3NduI07A69GBnzA+N6Lth3R/Od8ehO11sH2eN |
|
1083 EUYEh7+66LpcHu4OvcCe97Pmew4hZ9eI1az5QEln5yVZ7YxfGsXaJyeNzoGU156dDNqGpIJ2gZes |
|
1084 g2HsFZhk0tZaxJFKt8cTI4RAiTOMAT7Y2pola5PjFNcxC+u05aVBxmG01TERMkzqXMR0bb22ZJcK |
|
1085 pd5Ko6JOHNERbJjiqKP1R69DdD3K2OSCjw2CwwZgH0PA8GAL++DLlbGjIm2NRUN2CMlTGVd2Vlgj |
|
1086 EETQOSfkyUaJa5oaZUHlMe6djoavmbRLR0zxsWBfj2IuRjHffyXtv037Xxuu4oVnM9rgnDZkpTfa |
|
1087 ulSVgQ1YhYik15zTkzRlBcC7LElz8CpBaliATYJ6bgS6mbwqVgY1mmh15qzOhmLSgpN21lbfnbHS |
|
1088 FmFLir7YztSPU5fQIkKmqkMsMpswxUVAeyznhQKkwekaT0JwCfXgHyJGR5qmTlvAB89QOOdCH/bh |
|
1089 SEVbECYjilOoZh1jqka6sVM9m9KPN5MVxYkE7InyYtTzBe3f1s+ovbXaRKhJQIASVLbHS8plYDJw |
|
1090 cPVjWChnD4K4q/obn6a45svWpu7iVUm6+pjVUwnPLUy1SRLrnFieseGM9fI5k/8hzQFuZOkBwzyy |
|
1091 xhGtoJWre6LtKu080q41YYxq8olzrpypPo7qS0Wcc8TPFYcTZ0SecctKVn7FYmLc1vdNea/h/2dD |
|
1092 N2YO''' |
|
1093 |
|
1094 # "Python for S60" compiled resource as a base-64-encoded, zlib-compressed data |
|
1095 pythons60rscdata = ''' |
|
1096 eJzL9pIXYACCVnFWhouea4oYtRk4QHwWIGYMqAgEs6E0CEgxnOGAsRnYGRlYgXKcnK4VJal5xZn5 |
|
1097 eYJg8f9AwDDkgQSDAhDaMCQypDPkMhQzVDLUM7QydDNMZJjOMJdhMcNKhvUMWxl2MxxkOM5wluEy |
|
1098 w02G+wxPGV4zfGT4zvCXgZmRk5GfUZRRmhEAjnEjdg==''' |