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]) |
---|