Add some documentation to the SCons-version-switching hack
[senf.git] / scons / scons-1.2.0 / engine / SCons / Tool / packaging / msi.py
1 """SCons.Tool.packaging.msi
2
3 The msi packager.
4 """
5
6 #
7 # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008 The SCons Foundation
8
9 # Permission is hereby granted, free of charge, to any person obtaining
10 # a copy of this software and associated documentation files (the
11 # "Software"), to deal in the Software without restriction, including
12 # without limitation the rights to use, copy, modify, merge, publish,
13 # distribute, sublicense, and/or sell copies of the Software, and to
14 # permit persons to whom the Software is furnished to do so, subject to
15 # the following conditions:
16 #
17 # The above copyright notice and this permission notice shall be included
18 # in all copies or substantial portions of the Software.
19 #
20 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
21 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
22 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27 #
28
29 __revision__ = "src/engine/SCons/Tool/packaging/msi.py 3842 2008/12/20 22:59:52 scons"
30
31 import os
32 import SCons
33 from SCons.Action import Action
34 from SCons.Builder import Builder
35
36 from xml.dom.minidom import *
37 from xml.sax.saxutils import escape
38
39 from SCons.Tool.packaging import stripinstallbuilder
40
41 #
42 # Utility functions
43 #
44 def convert_to_id(s, id_set):
45     """ Some parts of .wxs need an Id attribute (for example: The File and
46     Directory directives. The charset is limited to A-Z, a-z, digits,
47     underscores, periods. Each Id must begin with a letter or with a
48     underscore. Google for "CNDL0015" for information about this.
49
50     Requirements:
51      * the string created must only contain chars from the target charset.
52      * the string created must have a minimal editing distance from the
53        original string.
54      * the string created must be unique for the whole .wxs file.
55
56     Observation:
57      * There are 62 chars in the charset.
58
59     Idea:
60      * filter out forbidden characters. Check for a collision with the help
61        of the id_set. Add the number of the number of the collision at the
62        end of the created string. Furthermore care for a correct start of
63        the string.
64     """
65     charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYabcdefghijklmnopqrstuvwxyz0123456789_.'
66     if s[0] in '0123456789.':
67         s += '_'+s
68     id = filter( lambda c : c in charset, s )
69
70     # did we already generate an id for this file?
71     try:
72         return id_set[id][s]
73     except KeyError:
74         # no we did not so initialize with the id
75         if not id_set.has_key(id): id_set[id] = { s : id }
76         # there is a collision, generate an id which is unique by appending
77         # the collision number
78         else: id_set[id][s] = id + str(len(id_set[id]))
79
80         return id_set[id][s]
81
82 def is_dos_short_file_name(file):
83     """ examine if the given file is in the 8.3 form.
84     """
85     fname, ext = os.path.splitext(file)
86     proper_ext = len(ext) == 0 or (2 <= len(ext) <= 4) # the ext contains the dot
87     proper_fname = file.isupper() and len(fname) <= 8
88
89     return proper_ext and proper_fname
90
91 def gen_dos_short_file_name(file, filename_set):
92     """ see http://support.microsoft.com/default.aspx?scid=kb;en-us;Q142982
93
94     These are no complete 8.3 dos short names. The ~ char is missing and 
95     replaced with one character from the filename. WiX warns about such
96     filenames, since a collision might occur. Google for "CNDL1014" for
97     more information.
98     """
99     # guard this to not confuse the generation
100     if is_dos_short_file_name(file):
101         return file
102
103     fname, ext = os.path.splitext(file) # ext contains the dot
104
105     # first try if it suffices to convert to upper
106     file = file.upper()
107     if is_dos_short_file_name(file):
108         return file
109
110     # strip forbidden characters.
111     forbidden = '."/[]:;=, '
112     fname = filter( lambda c : c not in forbidden, fname )
113
114     # check if we already generated a filename with the same number:
115     # thisis1.txt, thisis2.txt etc.
116     duplicate, num = not None, 1
117     while duplicate:
118         shortname = "%s%s" % (fname[:8-len(str(num))].upper(),\
119                               str(num))
120         if len(ext) >= 2:
121             shortname = "%s%s" % (shortname, ext[:4].upper())
122
123         duplicate, num = shortname in filename_set, num+1
124
125     assert( is_dos_short_file_name(shortname) ), 'shortname is %s, longname is %s' % (shortname, file)
126     filename_set.append(shortname)
127     return shortname
128
129 def create_feature_dict(files):
130     """ X_MSI_FEATURE and doc FileTag's can be used to collect files in a
131         hierarchy. This function collects the files into this hierarchy.
132     """
133     dict = {}
134
135     def add_to_dict( feature, file ):
136         if not SCons.Util.is_List( feature ):
137             feature = [ feature ]
138
139         for f in feature:
140             if not dict.has_key( f ):
141                 dict[ f ] = [ file ]
142             else:
143                 dict[ f ].append( file )
144
145     for file in files:
146         if hasattr( file, 'PACKAGING_X_MSI_FEATURE' ):
147             add_to_dict(file.PACKAGING_X_MSI_FEATURE, file)
148         elif hasattr( file, 'PACKAGING_DOC' ):
149             add_to_dict( 'PACKAGING_DOC', file )
150         else:
151             add_to_dict( 'default', file )
152
153     return dict
154
155 def generate_guids(root):
156     """ generates globally unique identifiers for parts of the xml which need
157     them.
158
159     Component tags have a special requirement. Their UUID is only allowed to
160     change if the list of their contained resources has changed. This allows
161     for clean removal and proper updates.
162
163     To handle this requirement, the uuid is generated with an md5 hashing the
164     whole subtree of a xml node.
165     """
166     from md5 import md5
167
168     # specify which tags need a guid and in which attribute this should be stored.
169     needs_id = { 'Product'   : 'Id',
170                  'Package'   : 'Id',
171                  'Component' : 'Guid',
172                }
173
174     # find all XMl nodes matching the key, retrieve their attribute, hash their
175     # subtree, convert hash to string and add as a attribute to the xml node.
176     for (key,value) in needs_id.items():
177         node_list = root.getElementsByTagName(key)
178         attribute = value
179         for node in node_list:
180             hash = md5(node.toxml()).hexdigest()
181             hash_str = '%s-%s-%s-%s-%s' % ( hash[:8], hash[8:12], hash[12:16], hash[16:20], hash[20:] )
182             node.attributes[attribute] = hash_str
183
184
185
186 def string_wxsfile(target, source, env):
187     return "building WiX file %s"%( target[0].path )
188
189 def build_wxsfile(target, source, env):
190     """ compiles a .wxs file from the keywords given in env['msi_spec'] and
191         by analyzing the tree of source nodes and their tags.
192     """
193     file = open(target[0].abspath, 'w')
194
195     try:
196         # Create a document with the Wix root tag
197         doc  = Document()
198         root = doc.createElement( 'Wix' )
199         root.attributes['xmlns']='http://schemas.microsoft.com/wix/2003/01/wi'
200         doc.appendChild( root )
201
202         filename_set = [] # this is to circumvent duplicates in the shortnames
203         id_set       = {} # this is to circumvent duplicates in the ids
204
205         # Create the content
206         build_wxsfile_header_section(root, env)
207         build_wxsfile_file_section(root, source, env['NAME'], env['VERSION'], env['VENDOR'], filename_set, id_set)
208         generate_guids(root)
209         build_wxsfile_features_section(root, source, env['NAME'], env['VERSION'], env['SUMMARY'], id_set)
210         build_wxsfile_default_gui(root)
211         build_license_file(target[0].get_dir(), env)
212
213         # write the xml to a file
214         file.write( doc.toprettyxml() )
215
216         # call a user specified function
217         if env.has_key('CHANGE_SPECFILE'):
218             env['CHANGE_SPECFILE'](target, source)
219
220     except KeyError, e:
221         raise SCons.Errors.UserError( '"%s" package field for MSI is missing.' % e.args[0] )
222
223 #
224 # setup function
225 #
226 def create_default_directory_layout(root, NAME, VERSION, VENDOR, filename_set):
227     """ Create the wix default target directory layout and return the innermost
228     directory.
229
230     We assume that the XML tree delivered in the root argument already contains
231     the Product tag.
232
233     Everything is put under the PFiles directory property defined by WiX.
234     After that a directory  with the 'VENDOR' tag is placed and then a
235     directory with the name of the project and its VERSION. This leads to the
236     following TARGET Directory Layout:
237     C:\<PFiles>\<Vendor>\<Projectname-Version>\
238     Example: C:\Programme\Company\Product-1.2\
239     """
240     doc = Document()
241     d1  = doc.createElement( 'Directory' )
242     d1.attributes['Id']   = 'TARGETDIR'
243     d1.attributes['Name'] = 'SourceDir'
244
245     d2  = doc.createElement( 'Directory' )
246     d2.attributes['Id']   = 'ProgramFilesFolder'
247     d2.attributes['Name'] = 'PFiles'
248
249     d3 = doc.createElement( 'Directory' )
250     d3.attributes['Id']       = 'VENDOR_folder'
251     d3.attributes['Name']     = escape( gen_dos_short_file_name( VENDOR, filename_set ) )
252     d3.attributes['LongName'] = escape( VENDOR )
253
254     d4 = doc.createElement( 'Directory' )
255     project_folder            = "%s-%s" % ( NAME, VERSION )
256     d4.attributes['Id']       = 'MY_DEFAULT_FOLDER'
257     d4.attributes['Name']     = escape( gen_dos_short_file_name( project_folder, filename_set ) )
258     d4.attributes['LongName'] = escape( project_folder )
259
260     d1.childNodes.append( d2 )
261     d2.childNodes.append( d3 )
262     d3.childNodes.append( d4 )
263
264     root.getElementsByTagName('Product')[0].childNodes.append( d1 )
265
266     return d4
267
268 #
269 # mandatory and optional file tags
270 #
271 def build_wxsfile_file_section(root, files, NAME, VERSION, VENDOR, filename_set, id_set):
272     """ builds the Component sections of the wxs file with their included files.
273
274     Files need to be specified in 8.3 format and in the long name format, long
275     filenames will be converted automatically.
276
277     Features are specficied with the 'X_MSI_FEATURE' or 'DOC' FileTag.
278     """
279     root       = create_default_directory_layout( root, NAME, VERSION, VENDOR, filename_set )
280     components = create_feature_dict( files )
281     factory    = Document()
282
283     def get_directory( node, dir ):
284         """ returns the node under the given node representing the directory.
285
286         Returns the component node if dir is None or empty.
287         """
288         if dir == '' or not dir:
289             return node
290
291         Directory = node
292         dir_parts = dir.split(os.path.sep)
293
294         # to make sure that our directory ids are unique, the parent folders are
295         # consecutively added to upper_dir
296         upper_dir = ''
297
298         # walk down the xml tree finding parts of the directory
299         dir_parts = filter( lambda d: d != '', dir_parts )
300         for d in dir_parts[:]:
301             already_created = filter( lambda c: c.nodeName == 'Directory' and c.attributes['LongName'].value == escape(d), Directory.childNodes ) 
302
303             if already_created != []:
304                 Directory = already_created[0]
305                 dir_parts.remove(d)
306                 upper_dir += d
307             else:
308                 break
309
310         for d in dir_parts:
311             nDirectory = factory.createElement( 'Directory' )
312             nDirectory.attributes['LongName'] = escape( d )
313             nDirectory.attributes['Name']     = escape( gen_dos_short_file_name( d, filename_set ) )
314             upper_dir += d
315             nDirectory.attributes['Id']       = convert_to_id( upper_dir, id_set )
316
317             Directory.childNodes.append( nDirectory )
318             Directory = nDirectory
319
320         return Directory
321
322     for file in files:
323         drive, path = os.path.splitdrive( file.PACKAGING_INSTALL_LOCATION )
324         filename = os.path.basename( path )
325         dirname  = os.path.dirname( path )
326
327         h = {
328             # tagname                   : default value
329             'PACKAGING_X_MSI_VITAL'     : 'yes',
330             'PACKAGING_X_MSI_FILEID'    : convert_to_id(filename, id_set),
331             'PACKAGING_X_MSI_LONGNAME'  : filename,
332             'PACKAGING_X_MSI_SHORTNAME' : gen_dos_short_file_name(filename, filename_set),
333             'PACKAGING_X_MSI_SOURCE'    : file.get_path(),
334             }
335
336         # fill in the default tags given above.
337         for k,v in [ (k, v) for (k,v) in h.items() if not hasattr(file, k) ]:
338             setattr( file, k, v )
339
340         File = factory.createElement( 'File' )
341         File.attributes['LongName'] = escape( file.PACKAGING_X_MSI_LONGNAME )
342         File.attributes['Name']     = escape( file.PACKAGING_X_MSI_SHORTNAME )
343         File.attributes['Source']   = escape( file.PACKAGING_X_MSI_SOURCE )
344         File.attributes['Id']       = escape( file.PACKAGING_X_MSI_FILEID )
345         File.attributes['Vital']    = escape( file.PACKAGING_X_MSI_VITAL )
346
347         # create the <Component> Tag under which this file should appear
348         Component = factory.createElement('Component')
349         Component.attributes['DiskId'] = '1'
350         Component.attributes['Id']     = convert_to_id( filename, id_set )
351
352         # hang the component node under the root node and the file node
353         # under the component node.
354         Directory = get_directory( root, dirname )
355         Directory.childNodes.append( Component )
356         Component.childNodes.append( File )
357
358 #
359 # additional functions
360 #
361 def build_wxsfile_features_section(root, files, NAME, VERSION, SUMMARY, id_set):
362     """ This function creates the <features> tag based on the supplied xml tree.
363
364     This is achieved by finding all <component>s and adding them to a default target.
365
366     It should be called after the tree has been built completly.  We assume
367     that a MY_DEFAULT_FOLDER Property is defined in the wxs file tree.
368
369     Furthermore a top-level with the name and VERSION of the software will be created.
370
371     An PACKAGING_X_MSI_FEATURE can either be a string, where the feature
372     DESCRIPTION will be the same as its title or a Tuple, where the first
373     part will be its title and the second its DESCRIPTION.
374     """
375     factory = Document()
376     Feature = factory.createElement('Feature')
377     Feature.attributes['Id']                    = 'complete'
378     Feature.attributes['ConfigurableDirectory'] = 'MY_DEFAULT_FOLDER'
379     Feature.attributes['Level']                 = '1'
380     Feature.attributes['Title']                 = escape( '%s %s' % (NAME, VERSION) )
381     Feature.attributes['Description']           = escape( SUMMARY )
382     Feature.attributes['Display']               = 'expand'
383
384     for (feature, files) in create_feature_dict(files).items():
385         SubFeature   = factory.createElement('Feature')
386         SubFeature.attributes['Level'] = '1'
387
388         if SCons.Util.is_Tuple(feature):
389             SubFeature.attributes['Id']    = convert_to_id( feature[0], id_set )
390             SubFeature.attributes['Title'] = escape(feature[0])
391             SubFeature.attributes['Description'] = escape(feature[1])
392         else:
393             SubFeature.attributes['Id'] = convert_to_id( feature, id_set )
394             if feature=='default':
395                 SubFeature.attributes['Description'] = 'Main Part'
396                 SubFeature.attributes['Title'] = 'Main Part'
397             elif feature=='PACKAGING_DOC':
398                 SubFeature.attributes['Description'] = 'Documentation'
399                 SubFeature.attributes['Title'] = 'Documentation'
400             else:
401                 SubFeature.attributes['Description'] = escape(feature)
402                 SubFeature.attributes['Title'] = escape(feature)
403
404         # build the componentrefs. As one of the design decision is that every
405         # file is also a component we walk the list of files and create a
406         # reference.
407         for f in files:
408             ComponentRef = factory.createElement('ComponentRef')
409             ComponentRef.attributes['Id'] = convert_to_id( os.path.basename(f.get_path()), id_set )
410             SubFeature.childNodes.append(ComponentRef)
411
412         Feature.childNodes.append(SubFeature)
413
414     root.getElementsByTagName('Product')[0].childNodes.append(Feature)
415
416 def build_wxsfile_default_gui(root):
417     """ this function adds a default GUI to the wxs file
418     """
419     factory = Document()
420     Product = root.getElementsByTagName('Product')[0]
421
422     UIRef   = factory.createElement('UIRef')
423     UIRef.attributes['Id'] = 'WixUI_Mondo'
424     Product.childNodes.append(UIRef)
425
426     UIRef   = factory.createElement('UIRef')
427     UIRef.attributes['Id'] = 'WixUI_ErrorProgressText'
428     Product.childNodes.append(UIRef)
429
430 def build_license_file(directory, spec):
431     """ creates a License.rtf file with the content of "X_MSI_LICENSE_TEXT"
432     in the given directory
433     """
434     name, text = '', ''
435
436     try:
437         name = spec['LICENSE']
438         text = spec['X_MSI_LICENSE_TEXT']
439     except KeyError:
440         pass # ignore this as X_MSI_LICENSE_TEXT is optional
441
442     if name!='' or text!='':
443         file = open( os.path.join(directory.get_path(), 'License.rtf'), 'w' )
444         file.write('{\\rtf')
445         if text!='':
446              file.write(text.replace('\n', '\\par '))
447         else:
448              file.write(name+'\\par\\par')
449         file.write('}')
450         file.close()
451
452 #
453 # mandatory and optional package tags
454 #
455 def build_wxsfile_header_section(root, spec):
456     """ Adds the xml file node which define the package meta-data.
457     """
458     # Create the needed DOM nodes and add them at the correct position in the tree.
459     factory = Document()
460     Product = factory.createElement( 'Product' )
461     Package = factory.createElement( 'Package' )
462
463     root.childNodes.append( Product )
464     Product.childNodes.append( Package )
465
466     # set "mandatory" default values
467     if not spec.has_key('X_MSI_LANGUAGE'):
468         spec['X_MSI_LANGUAGE'] = '1033' # select english
469
470     # mandatory sections, will throw a KeyError if the tag is not available
471     Product.attributes['Name']         = escape( spec['NAME'] )
472     Product.attributes['Version']      = escape( spec['VERSION'] )
473     Product.attributes['Manufacturer'] = escape( spec['VENDOR'] )
474     Product.attributes['Language']     = escape( spec['X_MSI_LANGUAGE'] )
475     Package.attributes['Description']  = escape( spec['SUMMARY'] )
476
477     # now the optional tags, for which we avoid the KeyErrror exception
478     if spec.has_key( 'DESCRIPTION' ):
479         Package.attributes['Comments'] = escape( spec['DESCRIPTION'] )
480
481     if spec.has_key( 'X_MSI_UPGRADE_CODE' ):
482         Package.attributes['X_MSI_UPGRADE_CODE'] = escape( spec['X_MSI_UPGRADE_CODE'] )
483
484     # We hardcode the media tag as our current model cannot handle it.
485     Media = factory.createElement('Media')
486     Media.attributes['Id']       = '1'
487     Media.attributes['Cabinet']  = 'default.cab'
488     Media.attributes['EmbedCab'] = 'yes'
489     root.getElementsByTagName('Product')[0].childNodes.append(Media)
490
491 # this builder is the entry-point for .wxs file compiler.
492 wxs_builder = Builder(
493     action         = Action( build_wxsfile, string_wxsfile ),
494     ensure_suffix  = '.wxs' )
495
496 def package(env, target, source, PACKAGEROOT, NAME, VERSION,
497             DESCRIPTION, SUMMARY, VENDOR, X_MSI_LANGUAGE, **kw):
498     # make sure that the Wix Builder is in the environment
499     SCons.Tool.Tool('wix').generate(env)
500
501     # get put the keywords for the specfile compiler. These are the arguments
502     # given to the package function and all optional ones stored in kw, minus
503     # the the source, target and env one.
504     loc = locals()
505     del loc['kw']
506     kw.update(loc)
507     del kw['source'], kw['target'], kw['env']
508
509     # strip the install builder from the source files
510     target, source = stripinstallbuilder(target, source, env)
511
512     # put the arguments into the env and call the specfile builder.
513     env['msi_spec'] = kw
514     specfile = apply( wxs_builder, [env, target, source], kw )
515
516     # now call the WiX Tool with the built specfile added as a source.
517     msifile  = env.WiX(target, specfile)
518
519     # return the target and source tuple.
520     return (msifile, source+[specfile])
521