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