summaryrefslogtreecommitdiffstats
path: root/Tools/msi/csv_to_wxs.py
diff options
context:
space:
mode:
authorSteve Dower <steve.dower@microsoft.com>2015-02-06 06:08:48 (GMT)
committerSteve Dower <steve.dower@microsoft.com>2015-02-06 06:08:48 (GMT)
commitbb24087a2cbfb186b540cc71a74ec8c39c1ebe3a (patch)
tree3b586e72b5744c4076a222e8f803a00d17b5f20f /Tools/msi/csv_to_wxs.py
parent7425f36ee74ccf5bfc2894aca6e6b86815894f98 (diff)
downloadcpython-bb24087a2cbfb186b540cc71a74ec8c39c1ebe3a.zip
cpython-bb24087a2cbfb186b540cc71a74ec8c39c1ebe3a.tar.gz
cpython-bb24087a2cbfb186b540cc71a74ec8c39c1ebe3a.tar.bz2
Issue #23260: Update Windows installer
Diffstat (limited to 'Tools/msi/csv_to_wxs.py')
-rw-r--r--Tools/msi/csv_to_wxs.py127
1 files changed, 127 insertions, 0 deletions
diff --git a/Tools/msi/csv_to_wxs.py b/Tools/msi/csv_to_wxs.py
new file mode 100644
index 0000000..235c8f8
--- /dev/null
+++ b/Tools/msi/csv_to_wxs.py
@@ -0,0 +1,127 @@
+'''
+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])