''' 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 " 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 = [ '', ' ', ] for dir_parent in sorted(directories): lines.append(' '.format(dir_parent)) for dir_name in sorted(directories[dir_parent]): lines.append(' '.format(dir_parent, make_id(dir_name), dir_name)) lines.append(' ') for dir_parent in (make_id(d) for group in cache_directories.values() for d in group): lines.append(' '.format(dir_parent)) lines.append(' '.format(dir_parent)) lines.append(' ') lines.append(' ') for group in sorted(groups): lines.extend([ ' ', ' '.format(group), ]) for source, target, disk_id, condition in groups[group]: lines.append(' '.format(make_id(target), make_id(target.parent))) if condition: lines.append(' {}'.format(condition)) if disk_id: lines.append(' '.format(make_id(target), target.name, source, disk_id)) else: lines.append(' '.format(make_id(target), target.name, source)) lines.append(' ') 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(' '.format(group, uuid1())) lines.extend(' '.format(p) for p in create_folders) lines.extend(' '.format(p) for p in create_folders) lines.extend(' '.format(p) for p in create_folders | remove_folders) lines.append(' ') lines.extend([ ' ', ' ', ]) lines.append('') # 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])