Steve Dower | bb24087 | 2015-02-05 22:08:48 -0800 | [diff] [blame] | 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]) |