From 26c2bd720f21ddb90f9ea3ec59dad6855cd4a2f4 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sat, 24 Sep 2022 10:59:44 +0100 Subject: [PATCH] Build: Enforce win32 only at top-level, and clean up how error is raised * Repeating the same test is just un-necessary, bad past-Ath. * Use `raise AssertionError(...)`. I'd move some of this stuff into functions, but it wouldn't actually aid readability. --- Build-exe-and-msi.py | 334 +++++++++++++++++++++---------------------- 1 file changed, 162 insertions(+), 172 deletions(-) diff --git a/Build-exe-and-msi.py b/Build-exe-and-msi.py index ab98d8a1..9cd04d53 100644 --- a/Build-exe-and-msi.py +++ b/Build-exe-and-msi.py @@ -22,12 +22,12 @@ if sys.version_info[0:2] != (3, 10): raise AssertionError(f'Unexpected python version {sys.version}') if sys.platform == 'win32': - assert platform.architecture()[0] == '32bit', 'Assumes a Python built for 32bit' + assert platform.architecture()[0] == '32bit', 'A Python 32bit build is required' import py2exe # noqa: F401 # Yes, this *is* used dist_dir = 'dist.win32' else: - assert False, f'Unsupported platform {sys.platform}' + raise AssertionError(f'Unsupported platform {sys.platform}') ########################################################################### ########################################################################### @@ -76,62 +76,57 @@ PLUGINS = [ 'plugins/inara.py', ] -# Ensure this fails on non-win32 -if sys.platform == 'win32': - OPTIONS = { - 'py2exe': { - 'dist_dir': dist_dir, - 'optimize': 2, - 'packages': [ - 'asyncio', # No longer auto as of py3.10+py2exe 0.11 - 'multiprocessing', # No longer auto as of py3.10+py2exe 0.11 - 'sqlite3', # Included for plugins - 'util', # 2022-02-01 only imported in plugins/eddn.py - ], - 'includes': [ - 'dataclasses', - 'shutil', # Included for plugins - 'timeout_session', - 'zipfile', # Included for plugins - ], - 'excludes': [ - 'distutils', - '_markerlib', - 'optparse', - 'PIL', - 'simplejson', - 'unittest' - ], - } +OPTIONS = { + 'py2exe': { + 'dist_dir': dist_dir, + 'optimize': 2, + 'packages': [ + 'asyncio', # No longer auto as of py3.10+py2exe 0.11 + 'multiprocessing', # No longer auto as of py3.10+py2exe 0.11 + 'sqlite3', # Included for plugins + 'util', # 2022-02-01 only imported in plugins/eddn.py + ], + 'includes': [ + 'dataclasses', + 'shutil', # Included for plugins + 'timeout_session', + 'zipfile', # Included for plugins + ], + 'excludes': [ + 'distutils', + '_markerlib', + 'optparse', + 'PIL', + 'simplejson', + 'unittest' + ], } +} - DATA_FILES = [ - ('', [ - '.gitversion', # Contains git short hash - 'WinSparkle.dll', - 'WinSparkle.pdb', # For debugging - don't include in package - 'EUROCAPS.TTF', - 'ChangeLog.md', - 'snd_good.wav', - 'snd_bad.wav', - 'modules.p', - 'ships.p', - f'{appname}.VisualElementsManifest.xml', - f'{appname}.ico', - 'EDMarketConnector - TRACE.bat', - 'EDMarketConnector - localserver-auth.bat', - 'EDMarketConnector - reset-ui.bat', - ]), - ('L10n', [join('L10n', x) for x in os.listdir('L10n') if x.endswith('.strings')]), - ('FDevIDs', [ - join('FDevIDs', 'commodity.csv'), - join('FDevIDs', 'rare_commodity.csv'), - ]), - ('plugins', PLUGINS), - ] - -else: - raise AssertionError('Unsupported platform') +DATA_FILES = [ + ('', [ + '.gitversion', # Contains git short hash + 'WinSparkle.dll', + 'WinSparkle.pdb', # For debugging - don't include in package + 'EUROCAPS.TTF', + 'ChangeLog.md', + 'snd_good.wav', + 'snd_bad.wav', + 'modules.p', + 'ships.p', + f'{appname}.VisualElementsManifest.xml', + f'{appname}.ico', + 'EDMarketConnector - TRACE.bat', + 'EDMarketConnector - localserver-auth.bat', + 'EDMarketConnector - reset-ui.bat', + ]), + ('L10n', [join('L10n', x) for x in os.listdir('L10n') if x.endswith('.strings')]), + ('FDevIDs', [ + join('FDevIDs', 'commodity.csv'), + join('FDevIDs', 'rare_commodity.csv'), + ]), + ('plugins', PLUGINS), +] ########################################################################### ########################################################################### @@ -172,136 +167,131 @@ freeze( # Build installer(s) ########################################################################### package_filename = None -# Ensure this fails on non-win32 -if sys.platform == 'win32': - template_file = pathlib.Path('wix/template.wxs') - components_file = pathlib.Path('wix/components.wxs') - final_wxs_file = pathlib.Path('EDMarketConnector.wxs') +template_file = pathlib.Path('wix/template.wxs') +components_file = pathlib.Path('wix/components.wxs') +final_wxs_file = pathlib.Path('EDMarketConnector.wxs') - # Use heat.exe to generate the Component for all files inside dist.win32 - os.system(rf'"{WIXPATH}\heat.exe" dir {dist_dir}\ -ag -sfrag -srid -suid -out {components_file}') +# Use heat.exe to generate the Component for all files inside dist.win32 +os.system(rf'"{WIXPATH}\heat.exe" dir {dist_dir}\ -ag -sfrag -srid -suid -out {components_file}') - component_tree = etree.parse(str(components_file)) - # 1. Change the element: - # - # - # - # to: - # - # - directory_win32 = component_tree.find('.//{*}Directory[@Id="dist.win32"][@Name="dist.win32"]') - if directory_win32 is None: - raise ValueError(f'{components_file}: Expected Directory with Id="dist.win32"') +component_tree = etree.parse(str(components_file)) +# 1. Change the element: +# +# +# +# to: +# +# +directory_win32 = component_tree.find('.//{*}Directory[@Id="dist.win32"][@Name="dist.win32"]') +if directory_win32 is None: + raise ValueError(f'{components_file}: Expected Directory with Id="dist.win32"') - directory_win32.set('Id', 'INSTALLDIR') - directory_win32.set('Name', '$(var.PRODUCTNAME)') - # 2. Change: - # - # - # - # - # - # to: - # - # - # - # - # - main_executable = directory_win32.find('.//{*}Component[@Id="EDMarketConnector.exe"]') - if main_executable is None: - raise ValueError(f'{components_file}: Expected Component with Id="EDMarketConnector.exe"') +directory_win32.set('Id', 'INSTALLDIR') +directory_win32.set('Name', '$(var.PRODUCTNAME)') +# 2. Change: +# +# +# +# +# +# to: +# +# +# +# +# +main_executable = directory_win32.find('.//{*}Component[@Id="EDMarketConnector.exe"]') +if main_executable is None: + raise ValueError(f'{components_file}: Expected Component with Id="EDMarketConnector.exe"') - main_executable.set('Id', 'MainExecutable') - main_executable.set('Guid', '{D33BB66E-9664-4AB6-A044-3004B50A09B0}') - shortcut = etree.SubElement( - main_executable, - 'Shortcut', - nsmap=main_executable.nsmap, +main_executable.set('Id', 'MainExecutable') +main_executable.set('Guid', '{D33BB66E-9664-4AB6-A044-3004B50A09B0}') +shortcut = etree.SubElement( + main_executable, + 'Shortcut', + nsmap=main_executable.nsmap, + attrib={ + 'Id': 'MainExeShortcut', + 'Directory': 'ProgramMenuFolder', + 'Name': '$(var.PRODUCTLONGNAME)', + 'Description': 'Downloads station data from Elite: Dangerous', + 'WorkingDirectory': 'INSTALLDIR', + 'Icon': 'EDMarketConnector.exe', + 'IconIndex': '0', + 'Advertise': 'yes' + } +) +# Now insert the appropriate parts as a child of the ProgramFilesFolder part +# of the template. +template_tree = etree.parse(str(template_file)) +program_files_folder = template_tree.find('.//{*}Directory[@Id="ProgramFilesFolder"]') +if program_files_folder is None: + raise ValueError(f'{template_file}: Expected Directory with Id="ProgramFilesFolder"') + +program_files_folder.insert(0, directory_win32) +# Append the Feature/ComponentRef listing to match +feature = template_tree.find('.//{*}Feature[@Id="Complete"][@Level="1"]') +if feature is None: + raise ValueError(f'{template_file}: Expected Feature element with Id="Complete" Level="1"') + +# This isn't part of the components +feature.append( + etree.Element( + 'ComponentRef', attrib={ - 'Id': 'MainExeShortcut', - 'Directory': 'ProgramMenuFolder', - 'Name': '$(var.PRODUCTLONGNAME)', - 'Description': 'Downloads station data from Elite: Dangerous', - 'WorkingDirectory': 'INSTALLDIR', - 'Icon': 'EDMarketConnector.exe', - 'IconIndex': '0', - 'Advertise': 'yes' - } + 'Id': 'RegistryEntries' + }, + nsmap=directory_win32.nsmap ) - # Now insert the appropriate parts as a child of the ProgramFilesFolder part - # of the template. - template_tree = etree.parse(str(template_file)) - program_files_folder = template_tree.find('.//{*}Directory[@Id="ProgramFilesFolder"]') - if program_files_folder is None: - raise ValueError(f'{template_file}: Expected Directory with Id="ProgramFilesFolder"') - - program_files_folder.insert(0, directory_win32) - # Append the Feature/ComponentRef listing to match - feature = template_tree.find('.//{*}Feature[@Id="Complete"][@Level="1"]') - if feature is None: - raise ValueError(f'{template_file}: Expected Feature element with Id="Complete" Level="1"') - - # This isn't part of the components +) +for c in directory_win32.findall('.//{*}Component'): feature.append( etree.Element( 'ComponentRef', attrib={ - 'Id': 'RegistryEntries' + 'Id': c.get('Id') }, nsmap=directory_win32.nsmap ) ) - for c in directory_win32.findall('.//{*}Component'): - feature.append( - etree.Element( - 'ComponentRef', - attrib={ - 'Id': c.get('Id') - }, - nsmap=directory_win32.nsmap - ) - ) - # Insert what we now have into the template and write it out - template_tree.write( - str(final_wxs_file), encoding='utf-8', - pretty_print=True, - xml_declaration=True +# Insert what we now have into the template and write it out +template_tree.write( + str(final_wxs_file), encoding='utf-8', + pretty_print=True, + xml_declaration=True +) + +os.system(rf'"{WIXPATH}\candle.exe" {appname}.wxs') + +if not exists(f'{appname}.wixobj'): + raise AssertionError(f'No {appname}.wixobj: candle.exe failed?') + +package_filename = f'{appname}_win_{appversion_nobuild()}.msi' +os.system(rf'"{WIXPATH}\light.exe" -b {dist_dir}\ -sacl -spdb -sw1076 {appname}.wixobj -out {package_filename}') + +if not exists(package_filename): + raise AssertionError(f'light.exe failed, no {package_filename}') + +# Seriously, this is how you make Windows Installer use the user's display language for its dialogs. What a crock. +# http://www.geektieguy.com/2010/03/13/create-a-multi-lingual-multi-language-msi-using-wix-and-custom-build-scripts +lcids = [ + int(x) for x in re.search( # type: ignore + r'Languages\s*=\s*"(.+?)"', + open(f'{appname}.wxs').read() + ).group(1).split(',') +] +assert lcids[0] == 1033, f'Default language is {lcids[0]}, should be 1033 (en_US)' +shutil.copyfile(package_filename, join(gettempdir(), f'{appname}_1033.msi')) +for lcid in lcids[1:]: + shutil.copyfile( + join(gettempdir(), f'{appname}_1033.msi'), + join(gettempdir(), f'{appname}_{lcid}.msi') ) - - os.system(rf'"{WIXPATH}\candle.exe" {appname}.wxs') - - if not exists(f'{appname}.wixobj'): - raise AssertionError(f'No {appname}.wixobj: candle.exe failed?') - - package_filename = f'{appname}_win_{appversion_nobuild()}.msi' - os.system(rf'"{WIXPATH}\light.exe" -b {dist_dir}\ -sacl -spdb -sw1076 {appname}.wixobj -out {package_filename}') - - if not exists(package_filename): - raise AssertionError(f'light.exe failed, no {package_filename}') - - # Seriously, this is how you make Windows Installer use the user's display language for its dialogs. What a crock. - # http://www.geektieguy.com/2010/03/13/create-a-multi-lingual-multi-language-msi-using-wix-and-custom-build-scripts - lcids = [ - int(x) for x in re.search( # type: ignore - r'Languages\s*=\s*"(.+?)"', - open(f'{appname}.wxs').read() - ).group(1).split(',') - ] - assert lcids[0] == 1033, f'Default language is {lcids[0]}, should be 1033 (en_US)' - shutil.copyfile(package_filename, join(gettempdir(), f'{appname}_1033.msi')) - for lcid in lcids[1:]: - shutil.copyfile( - join(gettempdir(), f'{appname}_1033.msi'), - join(gettempdir(), f'{appname}_{lcid}.msi') - ) - # Don't care about codepage because the displayed strings come from msiexec not our msi - os.system(rf'cscript /nologo "{SDKPATH}\WiLangId.vbs" {gettempdir()}\{appname}_{lcid}.msi Product {lcid}') - os.system(rf'"{SDKPATH}\MsiTran.Exe" -g {gettempdir()}\{appname}_1033.msi {gettempdir()}\{appname}_{lcid}.msi {gettempdir()}\{lcid}.mst') # noqa: E501 # Not going to get shorter - os.system(rf'cscript /nologo "{SDKPATH}\WiSubStg.vbs" {package_filename} {gettempdir()}\{lcid}.mst {lcid}') - -else: - raise AssertionError('Unsupported platform') + # Don't care about codepage because the displayed strings come from msiexec not our msi + os.system(rf'cscript /nologo "{SDKPATH}\WiLangId.vbs" {gettempdir()}\{appname}_{lcid}.msi Product {lcid}') + os.system(rf'"{SDKPATH}\MsiTran.Exe" -g {gettempdir()}\{appname}_1033.msi {gettempdir()}\{appname}_{lcid}.msi {gettempdir()}\{lcid}.mst') # noqa: E501 # Not going to get shorter + os.system(rf'cscript /nologo "{SDKPATH}\WiSubStg.vbs" {package_filename} {gettempdir()}\{lcid}.mst {lcid}') ###########################################################################