| ''' | 
 | Processes a CSV file containing a list of files into a WXS file with | 
 | components for each listed file. | 
 |  | 
 | The CSV columns are: | 
 |     source of file, target for file, group name | 
 |  | 
 | Usage:: | 
 |     py txt_to_wxs.py [path to file list .csv] [path to destination .wxs] | 
 |  | 
 | This is necessary to handle structures where some directories only | 
 | contain other directories. MSBuild is not able to generate the | 
 | Directory entries in the WXS file correctly, as it operates on files. | 
 | Python, however, can easily fill in the gap. | 
 | ''' | 
 |  | 
 | __author__ = "Steve Dower <steve.dower@microsoft.com>" | 
 |  | 
 | import csv | 
 | import re | 
 | import sys | 
 |  | 
 | from collections import defaultdict | 
 | from itertools import chain, zip_longest | 
 | from pathlib import PureWindowsPath | 
 | from uuid import uuid1 | 
 |  | 
 | ID_CHAR_SUBS = { | 
 |     '-': '_', | 
 |     '+': '_P', | 
 | } | 
 |  | 
 | def make_id(path): | 
 |     return re.sub( | 
 |         r'[^A-Za-z0-9_.]', | 
 |         lambda m: ID_CHAR_SUBS.get(m.group(0), '_'), | 
 |         str(path).rstrip('/\\'), | 
 |         flags=re.I | 
 |     ) | 
 |  | 
 | DIRECTORIES = set() | 
 |  | 
 | def main(file_source, install_target): | 
 |     with open(file_source, 'r', newline='') as f: | 
 |         files = list(csv.reader(f)) | 
 |  | 
 |     assert len(files) == len(set(make_id(f[1]) for f in files)), "Duplicate file IDs exist" | 
 |  | 
 |     directories = defaultdict(set) | 
 |     cache_directories = defaultdict(set) | 
 |     groups = defaultdict(list) | 
 |     for source, target, group, disk_id, condition in files: | 
 |         target = PureWindowsPath(target) | 
 |         groups[group].append((source, target, disk_id, condition)) | 
 |  | 
 |         if target.suffix.lower() in {".py", ".pyw"}: | 
 |             cache_directories[group].add(target.parent) | 
 |  | 
 |         for dirname in target.parents: | 
 |             parent = make_id(dirname.parent) | 
 |             if parent and parent != '.': | 
 |                 directories[parent].add(dirname.name) | 
 |  | 
 |     lines = [ | 
 |         '<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">', | 
 |         '    <Fragment>', | 
 |     ] | 
 |     for dir_parent in sorted(directories): | 
 |         lines.append('        <DirectoryRef Id="{}">'.format(dir_parent)) | 
 |         for dir_name in sorted(directories[dir_parent]): | 
 |             lines.append('            <Directory Id="{}_{}" Name="{}" />'.format(dir_parent, make_id(dir_name), dir_name)) | 
 |         lines.append('        </DirectoryRef>') | 
 |     for dir_parent in (make_id(d) for group in cache_directories.values() for d in group): | 
 |         lines.append('        <DirectoryRef Id="{}">'.format(dir_parent)) | 
 |         lines.append('            <Directory Id="{}___pycache__" Name="__pycache__" />'.format(dir_parent)) | 
 |         lines.append('        </DirectoryRef>') | 
 |     lines.append('    </Fragment>') | 
 |  | 
 |     for group in sorted(groups): | 
 |         lines.extend([ | 
 |             '    <Fragment>', | 
 |             '        <ComponentGroup Id="{}">'.format(group), | 
 |         ]) | 
 |         for source, target, disk_id, condition in groups[group]: | 
 |             lines.append('            <Component Id="{}" Directory="{}" Guid="*">'.format(make_id(target), make_id(target.parent))) | 
 |             if condition: | 
 |                 lines.append('                <Condition>{}</Condition>'.format(condition)) | 
 |  | 
 |             if disk_id: | 
 |                 lines.append('                <File Id="{}" Name="{}" Source="{}" DiskId="{}" />'.format(make_id(target), target.name, source, disk_id)) | 
 |             else: | 
 |                 lines.append('                <File Id="{}" Name="{}" Source="{}" />'.format(make_id(target), target.name, source)) | 
 |             lines.append('            </Component>') | 
 |  | 
 |         create_folders = {make_id(p) + "___pycache__" for p in cache_directories[group]} | 
 |         remove_folders = {make_id(p2) for p1 in cache_directories[group] for p2 in chain((p1,), p1.parents)} | 
 |         create_folders.discard(".") | 
 |         remove_folders.discard(".") | 
 |         if create_folders or remove_folders: | 
 |             lines.append('            <Component Id="{}__pycache__folders" Directory="TARGETDIR" Guid="{}">'.format(group, uuid1())) | 
 |             lines.extend('                <CreateFolder Directory="{}" />'.format(p) for p in create_folders) | 
 |             lines.extend('                <RemoveFile Id="Remove_{0}_files" Name="*" On="uninstall" Directory="{0}" />'.format(p) for p in create_folders) | 
 |             lines.extend('                <RemoveFolder Id="Remove_{0}_folder" On="uninstall" Directory="{0}" />'.format(p) for p in create_folders | remove_folders) | 
 |             lines.append('            </Component>') | 
 |  | 
 |         lines.extend([ | 
 |             '        </ComponentGroup>', | 
 |             '    </Fragment>', | 
 |         ]) | 
 |     lines.append('</Wix>') | 
 |  | 
 |     # Check if the file matches. If so, we don't want to touch it so | 
 |     # that we can skip rebuilding. | 
 |     try: | 
 |         with open(install_target, 'r') as f: | 
 |             if all(x.rstrip('\r\n') == y for x, y in zip_longest(f, lines)): | 
 |                 print('File is up to date') | 
 |                 return | 
 |     except IOError: | 
 |         pass | 
 |  | 
 |     with open(install_target, 'w') as f: | 
 |         f.writelines(line + '\n' for line in lines) | 
 |     print('Wrote {} lines to {}'.format(len(lines), install_target)) | 
 |  | 
 | if __name__ == '__main__': | 
 |     main(sys.argv[1], sys.argv[2]) |