1 """SCons.Tool.packaging.msi
7 # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008 The SCons Foundation
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:
17 # The above copyright notice and this permission notice shall be included
18 # in all copies or substantial portions of the Software.
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.
29 __revision__ = "src/engine/SCons/Tool/packaging/msi.py 3842 2008/12/20 22:59:52 scons"
33 from SCons.Action import Action
34 from SCons.Builder import Builder
36 from xml.dom.minidom import *
37 from xml.sax.saxutils import escape
39 from SCons.Tool.packaging import stripinstallbuilder
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.
51 * the string created must only contain chars from the target charset.
52 * the string created must have a minimal editing distance from the
54 * the string created must be unique for the whole .wxs file.
57 * There are 62 chars in the charset.
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
65 charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYabcdefghijklmnopqrstuvwxyz0123456789_.'
66 if s[0] in '0123456789.':
68 id = filter( lambda c : c in charset, s )
70 # did we already generate an id for this file?
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]))
82 def is_dos_short_file_name(file):
83 """ examine if the given file is in the 8.3 form.
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
89 return proper_ext and proper_fname
91 def gen_dos_short_file_name(file, filename_set):
92 """ see http://support.microsoft.com/default.aspx?scid=kb;en-us;Q142982
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
99 # guard this to not confuse the generation
100 if is_dos_short_file_name(file):
103 fname, ext = os.path.splitext(file) # ext contains the dot
105 # first try if it suffices to convert to upper
107 if is_dos_short_file_name(file):
110 # strip forbidden characters.
111 forbidden = '."/[]:;=, '
112 fname = filter( lambda c : c not in forbidden, fname )
114 # check if we already generated a filename with the same number:
115 # thisis1.txt, thisis2.txt etc.
116 duplicate, num = not None, 1
118 shortname = "%s%s" % (fname[:8-len(str(num))].upper(),\
121 shortname = "%s%s" % (shortname, ext[:4].upper())
123 duplicate, num = shortname in filename_set, num+1
125 assert( is_dos_short_file_name(shortname) ), 'shortname is %s, longname is %s' % (shortname, file)
126 filename_set.append(shortname)
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.
135 def add_to_dict( feature, file ):
136 if not SCons.Util.is_List( feature ):
137 feature = [ feature ]
140 if not dict.has_key( f ):
143 dict[ f ].append( file )
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 )
151 add_to_dict( 'default', file )
155 def generate_guids(root):
156 """ generates globally unique identifiers for parts of the xml which need
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.
163 To handle this requirement, the uuid is generated with an md5 hashing the
164 whole subtree of a xml node.
168 # specify which tags need a guid and in which attribute this should be stored.
169 needs_id = { 'Product' : 'Id',
171 'Component' : 'Guid',
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)
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
186 def string_wxsfile(target, source, env):
187 return "building WiX file %s"%( target[0].path )
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.
193 file = open(target[0].abspath, 'w')
196 # Create a document with the Wix root tag
198 root = doc.createElement( 'Wix' )
199 root.attributes['xmlns']='http://schemas.microsoft.com/wix/2003/01/wi'
200 doc.appendChild( root )
202 filename_set = [] # this is to circumvent duplicates in the shortnames
203 id_set = {} # this is to circumvent duplicates in the ids
206 build_wxsfile_header_section(root, env)
207 build_wxsfile_file_section(root, source, env['NAME'], env['VERSION'], env['VENDOR'], filename_set, id_set)
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)
213 # write the xml to a file
214 file.write( doc.toprettyxml() )
216 # call a user specified function
217 if env.has_key('CHANGE_SPECFILE'):
218 env['CHANGE_SPECFILE'](target, source)
221 raise SCons.Errors.UserError( '"%s" package field for MSI is missing.' % e.args[0] )
226 def create_default_directory_layout(root, NAME, VERSION, VENDOR, filename_set):
227 """ Create the wix default target directory layout and return the innermost
230 We assume that the XML tree delivered in the root argument already contains
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\
241 d1 = doc.createElement( 'Directory' )
242 d1.attributes['Id'] = 'TARGETDIR'
243 d1.attributes['Name'] = 'SourceDir'
245 d2 = doc.createElement( 'Directory' )
246 d2.attributes['Id'] = 'ProgramFilesFolder'
247 d2.attributes['Name'] = 'PFiles'
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 )
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 )
260 d1.childNodes.append( d2 )
261 d2.childNodes.append( d3 )
262 d3.childNodes.append( d4 )
264 root.getElementsByTagName('Product')[0].childNodes.append( d1 )
269 # mandatory and optional file tags
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.
274 Files need to be specified in 8.3 format and in the long name format, long
275 filenames will be converted automatically.
277 Features are specficied with the 'X_MSI_FEATURE' or 'DOC' FileTag.
279 root = create_default_directory_layout( root, NAME, VERSION, VENDOR, filename_set )
280 components = create_feature_dict( files )
283 def get_directory( node, dir ):
284 """ returns the node under the given node representing the directory.
286 Returns the component node if dir is None or empty.
288 if dir == '' or not dir:
292 dir_parts = dir.split(os.path.sep)
294 # to make sure that our directory ids are unique, the parent folders are
295 # consecutively added to upper_dir
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 )
303 if already_created != []:
304 Directory = already_created[0]
311 nDirectory = factory.createElement( 'Directory' )
312 nDirectory.attributes['LongName'] = escape( d )
313 nDirectory.attributes['Name'] = escape( gen_dos_short_file_name( d, filename_set ) )
315 nDirectory.attributes['Id'] = convert_to_id( upper_dir, id_set )
317 Directory.childNodes.append( nDirectory )
318 Directory = nDirectory
323 drive, path = os.path.splitdrive( file.PACKAGING_INSTALL_LOCATION )
324 filename = os.path.basename( path )
325 dirname = os.path.dirname( path )
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(),
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 )
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 )
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 )
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 )
359 # additional functions
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.
364 This is achieved by finding all <component>s and adding them to a default target.
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.
369 Furthermore a top-level with the name and VERSION of the software will be created.
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.
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'
384 for (feature, files) in create_feature_dict(files).items():
385 SubFeature = factory.createElement('Feature')
386 SubFeature.attributes['Level'] = '1'
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])
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'
401 SubFeature.attributes['Description'] = escape(feature)
402 SubFeature.attributes['Title'] = escape(feature)
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
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)
412 Feature.childNodes.append(SubFeature)
414 root.getElementsByTagName('Product')[0].childNodes.append(Feature)
416 def build_wxsfile_default_gui(root):
417 """ this function adds a default GUI to the wxs file
420 Product = root.getElementsByTagName('Product')[0]
422 UIRef = factory.createElement('UIRef')
423 UIRef.attributes['Id'] = 'WixUI_Mondo'
424 Product.childNodes.append(UIRef)
426 UIRef = factory.createElement('UIRef')
427 UIRef.attributes['Id'] = 'WixUI_ErrorProgressText'
428 Product.childNodes.append(UIRef)
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
437 name = spec['LICENSE']
438 text = spec['X_MSI_LICENSE_TEXT']
440 pass # ignore this as X_MSI_LICENSE_TEXT is optional
442 if name!='' or text!='':
443 file = open( os.path.join(directory.get_path(), 'License.rtf'), 'w' )
446 file.write(text.replace('\n', '\\par '))
448 file.write(name+'\\par\\par')
453 # mandatory and optional package tags
455 def build_wxsfile_header_section(root, spec):
456 """ Adds the xml file node which define the package meta-data.
458 # Create the needed DOM nodes and add them at the correct position in the tree.
460 Product = factory.createElement( 'Product' )
461 Package = factory.createElement( 'Package' )
463 root.childNodes.append( Product )
464 Product.childNodes.append( Package )
466 # set "mandatory" default values
467 if not spec.has_key('X_MSI_LANGUAGE'):
468 spec['X_MSI_LANGUAGE'] = '1033' # select english
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'] )
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'] )
481 if spec.has_key( 'X_MSI_UPGRADE_CODE' ):
482 Package.attributes['X_MSI_UPGRADE_CODE'] = escape( spec['X_MSI_UPGRADE_CODE'] )
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)
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' )
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)
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.
507 del kw['source'], kw['target'], kw['env']
509 # strip the install builder from the source files
510 target, source = stripinstallbuilder(target, source, env)
512 # put the arguments into the env and call the specfile builder.
514 specfile = apply( wxs_builder, [env, target, source], kw )
516 # now call the WiX Tool with the built specfile added as a source.
517 msifile = env.WiX(target, specfile)
519 # return the target and source tuple.
520 return (msifile, source+[specfile])