(root)/
Python-3.11.7/
Tools/
msi/
csv_to_wxs.py
       1  '''
       2  Processes a CSV file containing a list of files into a WXS file with
       3  components for each listed file.
       4  
       5  The CSV columns are:
       6      source of file, target for file, group name
       7  
       8  Usage::
       9      py txt_to_wxs.py [path to file list .csv] [path to destination .wxs]
      10  
      11  This is necessary to handle structures where some directories only
      12  contain other directories. MSBuild is not able to generate the
      13  Directory entries in the WXS file correctly, as it operates on files.
      14  Python, however, can easily fill in the gap.
      15  '''
      16  
      17  __author__ = "Steve Dower <steve.dower@microsoft.com>"
      18  
      19  import csv
      20  import re
      21  import sys
      22  
      23  from collections import defaultdict
      24  from itertools import chain, zip_longest
      25  from pathlib import PureWindowsPath
      26  from uuid import uuid1
      27  
      28  ID_CHAR_SUBS = {
      29      '-': '_',
      30      '+': '_P',
      31  }
      32  
      33  def make_id(path):
      34      return re.sub(
      35          r'[^A-Za-z0-9_.]',
      36          lambda m: ID_CHAR_SUBS.get(m.group(0), '_'),
      37          str(path).rstrip('/\\'),
      38          flags=re.I
      39      )
      40  
      41  DIRECTORIES = set()
      42  
      43  def main(file_source, install_target):
      44      with open(file_source, 'r', newline='') as f:
      45          files = list(csv.reader(f))
      46  
      47      assert len(files) == len(set(make_id(f[1]) for f in files)), "Duplicate file IDs exist"
      48  
      49      directories = defaultdict(set)
      50      cache_directories = defaultdict(set)
      51      groups = defaultdict(list)
      52      for source, target, group, disk_id, condition in files:
      53          target = PureWindowsPath(target)
      54          groups[group].append((source, target, disk_id, condition))
      55  
      56          if target.suffix.lower() in {".py", ".pyw"}:
      57              cache_directories[group].add(target.parent)
      58  
      59          for dirname in target.parents:
      60              parent = make_id(dirname.parent)
      61              if parent and parent != '.':
      62                  directories[parent].add(dirname.name)
      63  
      64      lines = [
      65          '<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">',
      66          '    <Fragment>',
      67      ]
      68      for dir_parent in sorted(directories):
      69          lines.append('        <DirectoryRef Id="{}">'.format(dir_parent))
      70          for dir_name in sorted(directories[dir_parent]):
      71              lines.append('            <Directory Id="{}_{}" Name="{}" />'.format(dir_parent, make_id(dir_name), dir_name))
      72          lines.append('        </DirectoryRef>')
      73      for dir_parent in (make_id(d) for group in cache_directories.values() for d in group):
      74          lines.append('        <DirectoryRef Id="{}">'.format(dir_parent))
      75          lines.append('            <Directory Id="{}___pycache__" Name="__pycache__" />'.format(dir_parent))
      76          lines.append('        </DirectoryRef>')
      77      lines.append('    </Fragment>')
      78  
      79      for group in sorted(groups):
      80          lines.extend([
      81              '    <Fragment>',
      82              '        <ComponentGroup Id="{}">'.format(group),
      83          ])
      84          for source, target, disk_id, condition in groups[group]:
      85              lines.append('            <Component Id="{}" Directory="{}" Guid="*">'.format(make_id(target), make_id(target.parent)))
      86              if condition:
      87                  lines.append('                <Condition>{}</Condition>'.format(condition))
      88  
      89              if disk_id:
      90                  lines.append('                <File Id="{}" Name="{}" Source="{}" DiskId="{}" />'.format(make_id(target), target.name, source, disk_id))
      91              else:
      92                  lines.append('                <File Id="{}" Name="{}" Source="{}" />'.format(make_id(target), target.name, source))
      93              lines.append('            </Component>')
      94  
      95          create_folders = {make_id(p) + "___pycache__" for p in cache_directories[group]}
      96          remove_folders = {make_id(p2) for p1 in cache_directories[group] for p2 in chain((p1,), p1.parents)}
      97          create_folders.discard(".")
      98          remove_folders.discard(".")
      99          if create_folders or remove_folders:
     100              lines.append('            <Component Id="{}__pycache__folders" Directory="TARGETDIR" Guid="{}">'.format(group, uuid1()))
     101              lines.extend('                <CreateFolder Directory="{}" />'.format(p) for p in create_folders)
     102              lines.extend('                <RemoveFile Id="Remove_{0}_files" Name="*" On="uninstall" Directory="{0}" />'.format(p) for p in create_folders)
     103              lines.extend('                <RemoveFolder Id="Remove_{0}_folder" On="uninstall" Directory="{0}" />'.format(p) for p in create_folders | remove_folders)
     104              lines.append('            </Component>')
     105  
     106          lines.extend([
     107              '        </ComponentGroup>',
     108              '    </Fragment>',
     109          ])
     110      lines.append('</Wix>')
     111  
     112      # Check if the file matches. If so, we don't want to touch it so
     113      # that we can skip rebuilding.
     114      try:
     115          with open(install_target, 'r') as f:
     116              if all(x.rstrip('\r\n') == y for x, y in zip_longest(f, lines)):
     117                  print('File is up to date')
     118                  return
     119      except IOError:
     120          pass
     121  
     122      with open(install_target, 'w') as f:
     123          f.writelines(line + '\n' for line in lines)
     124      print('Wrote {} lines to {}'.format(len(lines), install_target))
     125  
     126  if __name__ == '__main__':
     127      main(sys.argv[1], sys.argv[2])