128 lines
4.9 KiB
Python
128 lines
4.9 KiB
Python
|
'''
|
||
|
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])
|