"Fossies" - the Fresh Open Source Software Archive

Member "scons-3.0.5/engine/SCons/Tool/packaging/msi.py" (26 Mar 2019, 20241 Bytes) of package /linux/misc/scons-3.0.5.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "msi.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 3.0.4_vs_3.0.5.

    1 """SCons.Tool.packaging.msi
    2 
    3 The msi packager.
    4 """
    5 
    6 #
    7 # Copyright (c) 2001 - 2019 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 __revision__ = "src/engine/SCons/Tool/packaging/msi.py a56bbd8c09fb219ab8a9673330ffcd55279219d0 2019-03-26 23:16:31 bdeegan"
   29 
   30 import os
   31 import SCons
   32 from SCons.Action import Action
   33 from SCons.Builder import Builder
   34 
   35 from xml.dom.minidom import *
   36 from xml.sax.saxutils import escape
   37 
   38 from SCons.Tool.packaging import stripinstallbuilder
   39 
   40 #
   41 # Utility functions
   42 #
   43 def convert_to_id(s, id_set):
   44     """ Some parts of .wxs need an Id attribute (for example: The File and
   45     Directory directives. The charset is limited to A-Z, a-z, digits,
   46     underscores, periods. Each Id must begin with a letter or with a
   47     underscore. Google for "CNDL0015" for information about this.
   48 
   49     Requirements:
   50      * the string created must only contain chars from the target charset.
   51      * the string created must have a minimal editing distance from the
   52        original string.
   53      * the string created must be unique for the whole .wxs file.
   54 
   55     Observation:
   56      * There are 62 chars in the charset.
   57 
   58     Idea:
   59      * filter out forbidden characters. Check for a collision with the help
   60        of the id_set. Add the number of the number of the collision at the
   61        end of the created string. Furthermore care for a correct start of
   62        the string.
   63     """
   64     charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYabcdefghijklmnopqrstuvwxyz0123456789_.'
   65     if s[0] in '0123456789.':
   66         s = '_' + s
   67     id = ''.join([c for c in s if c in charset])
   68 
   69     # did we already generate an id for this file?
   70     try:
   71         return id_set[id][s]
   72     except KeyError:
   73         # no we did not, so initialize with the id
   74         if id not in id_set: id_set[id] = { s : id }
   75         # there is a collision, generate an id which is unique by appending
   76         # the collision number
   77         else: id_set[id][s] = id + str(len(id_set[id]))
   78 
   79         return id_set[id][s]
   80 
   81 def is_dos_short_file_name(file):
   82     """ Examine if the given file is in the 8.3 form.
   83     """
   84     fname, ext = os.path.splitext(file)
   85     proper_ext = len(ext) == 0 or (2 <= len(ext) <= 4) # the ext contains the dot
   86     proper_fname = file.isupper() and len(fname) <= 8
   87 
   88     return proper_ext and proper_fname
   89 
   90 def gen_dos_short_file_name(file, filename_set):
   91     """ See http://support.microsoft.com/default.aspx?scid=kb;en-us;Q142982
   92 
   93     These are no complete 8.3 dos short names. The ~ char is missing and 
   94     replaced with one character from the filename. WiX warns about such
   95     filenames, since a collision might occur. Google for "CNDL1014" for
   96     more information.
   97     """
   98     # guard this to not confuse the generation
   99     if is_dos_short_file_name(file):
  100         return file
  101 
  102     fname, ext = os.path.splitext(file) # ext contains the dot
  103 
  104     # first try if it suffices to convert to upper
  105     file = file.upper()
  106     if is_dos_short_file_name(file):
  107         return file
  108 
  109     # strip forbidden characters.
  110     forbidden = '."/[]:;=, '
  111     fname = ''.join([c for c in fname if c not in forbidden])
  112 
  113     # check if we already generated a filename with the same number:
  114     # thisis1.txt, thisis2.txt etc.
  115     duplicate, num = not None, 1
  116     while duplicate:
  117         shortname = "%s%s" % (fname[:8-len(str(num))].upper(), str(num))
  118         if len(ext) >= 2:
  119             shortname = "%s%s" % (shortname, ext[:4].upper())
  120 
  121         duplicate, num = shortname in filename_set, num+1
  122 
  123     assert( is_dos_short_file_name(shortname) ), 'shortname is %s, longname is %s' % (shortname, file)
  124     filename_set.append(shortname)
  125     return shortname
  126 
  127 def create_feature_dict(files):
  128     """ X_MSI_FEATURE and doc FileTag's can be used to collect files in a
  129         hierarchy. This function collects the files into this hierarchy.
  130     """
  131     dict = {}
  132 
  133     def add_to_dict( feature, file ):
  134         if not SCons.Util.is_List( feature ):
  135             feature = [ feature ]
  136 
  137         for f in feature:
  138             if f not in dict:
  139                 dict[ f ] = [ file ]
  140             else:
  141                 dict[ f ].append( file )
  142 
  143     for file in files:
  144         if hasattr( file, 'PACKAGING_X_MSI_FEATURE' ):
  145             add_to_dict(file.PACKAGING_X_MSI_FEATURE, file)
  146         elif hasattr( file, 'PACKAGING_DOC' ):
  147             add_to_dict( 'PACKAGING_DOC', file )
  148         else:
  149             add_to_dict( 'default', file )
  150 
  151     return dict
  152 
  153 def generate_guids(root):
  154     """ generates globally unique identifiers for parts of the xml which need
  155     them.
  156 
  157     Component tags have a special requirement. Their UUID is only allowed to
  158     change if the list of their contained resources has changed. This allows
  159     for clean removal and proper updates.
  160 
  161     To handle this requirement, the uuid is generated with an md5 hashing the
  162     whole subtree of a xml node.
  163     """
  164     from hashlib import md5
  165 
  166     # specify which tags need a guid and in which attribute this should be stored.
  167     needs_id = { 'Product'   : 'Id',
  168                  'Package'   : 'Id',
  169                  'Component' : 'Guid',
  170                }
  171 
  172     # find all XMl nodes matching the key, retrieve their attribute, hash their
  173     # subtree, convert hash to string and add as a attribute to the xml node.
  174     for (key,value) in needs_id.items():
  175         node_list = root.getElementsByTagName(key)
  176         attribute = value
  177         for node in node_list:
  178             hash = md5(node.toxml()).hexdigest()
  179             hash_str = '%s-%s-%s-%s-%s' % ( hash[:8], hash[8:12], hash[12:16], hash[16:20], hash[20:] )
  180             node.attributes[attribute] = hash_str
  181 
  182 
  183 
  184 def string_wxsfile(target, source, env):
  185     return "building WiX file %s"%( target[0].path )
  186 
  187 def build_wxsfile(target, source, env):
  188     """ Compiles a .wxs file from the keywords given in env['msi_spec'] and
  189         by analyzing the tree of source nodes and their tags.
  190     """
  191     file = open(target[0].get_abspath(), 'w')
  192 
  193     try:
  194         # Create a document with the Wix root tag
  195         doc  = Document()
  196         root = doc.createElement( 'Wix' )
  197         root.attributes['xmlns']='http://schemas.microsoft.com/wix/2003/01/wi'
  198         doc.appendChild( root )
  199 
  200         filename_set = [] # this is to circumvent duplicates in the shortnames
  201         id_set       = {} # this is to circumvent duplicates in the ids
  202 
  203         # Create the content
  204         build_wxsfile_header_section(root, env)
  205         build_wxsfile_file_section(root, source, env['NAME'], env['VERSION'], env['VENDOR'], filename_set, id_set)
  206         generate_guids(root)
  207         build_wxsfile_features_section(root, source, env['NAME'], env['VERSION'], env['SUMMARY'], id_set)
  208         build_wxsfile_default_gui(root)
  209         build_license_file(target[0].get_dir(), env)
  210 
  211         # write the xml to a file
  212         file.write( doc.toprettyxml() )
  213 
  214         # call a user specified function
  215         if 'CHANGE_SPECFILE' in env:
  216             env['CHANGE_SPECFILE'](target, source)
  217 
  218     except KeyError as e:
  219         raise SCons.Errors.UserError( '"%s" package field for MSI is missing.' % e.args[0] )
  220 
  221 #
  222 # setup function
  223 #
  224 def create_default_directory_layout(root, NAME, VERSION, VENDOR, filename_set):
  225     """ Create the wix default target directory layout and return the innermost
  226     directory.
  227 
  228     We assume that the XML tree delivered in the root argument already contains
  229     the Product tag.
  230 
  231     Everything is put under the PFiles directory property defined by WiX.
  232     After that a directory  with the 'VENDOR' tag is placed and then a
  233     directory with the name of the project and its VERSION. This leads to the
  234     following TARGET Directory Layout:
  235     C:\<PFiles>\<Vendor>\<Projectname-Version>\
  236     Example: C:\Programme\Company\Product-1.2\
  237     """
  238     doc = Document()
  239     d1  = doc.createElement( 'Directory' )
  240     d1.attributes['Id']   = 'TARGETDIR'
  241     d1.attributes['Name'] = 'SourceDir'
  242 
  243     d2  = doc.createElement( 'Directory' )
  244     d2.attributes['Id']   = 'ProgramFilesFolder'
  245     d2.attributes['Name'] = 'PFiles'
  246 
  247     d3 = doc.createElement( 'Directory' )
  248     d3.attributes['Id']       = 'VENDOR_folder'
  249     d3.attributes['Name']     = escape( gen_dos_short_file_name( VENDOR, filename_set ) )
  250     d3.attributes['LongName'] = escape( VENDOR )
  251 
  252     d4 = doc.createElement( 'Directory' )
  253     project_folder            = "%s-%s" % ( NAME, VERSION )
  254     d4.attributes['Id']       = 'MY_DEFAULT_FOLDER'
  255     d4.attributes['Name']     = escape( gen_dos_short_file_name( project_folder, filename_set ) )
  256     d4.attributes['LongName'] = escape( project_folder )
  257 
  258     d1.childNodes.append( d2 )
  259     d2.childNodes.append( d3 )
  260     d3.childNodes.append( d4 )
  261 
  262     root.getElementsByTagName('Product')[0].childNodes.append( d1 )
  263 
  264     return d4
  265 
  266 #
  267 # mandatory and optional file tags
  268 #
  269 def build_wxsfile_file_section(root, files, NAME, VERSION, VENDOR, filename_set, id_set):
  270     """ Builds the Component sections of the wxs file with their included files.
  271 
  272     Files need to be specified in 8.3 format and in the long name format, long
  273     filenames will be converted automatically.
  274 
  275     Features are specficied with the 'X_MSI_FEATURE' or 'DOC' FileTag.
  276     """
  277     root       = create_default_directory_layout( root, NAME, VERSION, VENDOR, filename_set )
  278     components = create_feature_dict( files )
  279     factory    = Document()
  280 
  281     def get_directory( node, dir ):
  282         """ Returns the node under the given node representing the directory.
  283 
  284         Returns the component node if dir is None or empty.
  285         """
  286         if dir == '' or not dir:
  287             return node
  288 
  289         Directory = node
  290         dir_parts = dir.split(os.path.sep)
  291 
  292         # to make sure that our directory ids are unique, the parent folders are
  293         # consecutively added to upper_dir
  294         upper_dir = ''
  295 
  296         # walk down the xml tree finding parts of the directory
  297         dir_parts = [d for d in dir_parts if d != '']
  298         for d in dir_parts[:]:
  299             already_created = [c for c in Directory.childNodes
  300                                if c.nodeName == 'Directory'
  301                                and c.attributes['LongName'].value == escape(d)] 
  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 'X_MSI_LANGUAGE' not in spec:
  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 'DESCRIPTION' in spec:
  479         Package.attributes['Comments'] = escape( spec['DESCRIPTION'] )
  480 
  481     if 'X_MSI_UPGRADE_CODE' in spec:
  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 = 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 
  522 # Local Variables:
  523 # tab-width:4
  524 # indent-tabs-mode:nil
  525 # End:
  526 # vim: set expandtab tabstop=4 shiftwidth=4: