From 6ad6c90213f34d318d9694255cfd4e298527a69c Mon Sep 17 00:00:00 2001 From: Athanasius Date: Tue, 6 Sep 2022 16:49:57 +0100 Subject: [PATCH 1/8] First cut at utilising a possible new py2exe API/paradigm See https://github.com/py2exe/py2exe/issues/127 for context This filename is still open to change, I don't want to just use `freeze.py`, as that seems too potentially ambiguous. --- py2exe-build.py | 418 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 py2exe-build.py diff --git a/py2exe-build.py b/py2exe-build.py new file mode 100644 index 00000000..e54489ef --- /dev/null +++ b/py2exe-build.py @@ -0,0 +1,418 @@ +#!/usr/bin/env python3 +"""Build to executables and MSI installer using py2exe and other tools.""" +import codecs +import os +import pathlib +import platform +import re +import shutil +import sys +from os.path import exists, isdir, join +from tempfile import gettempdir +from typing import Any, Generator, Set + +from lxml import etree +from py2exe import freeze + +from config import ( + appcmdname, applongname, appname, appversion, appversion_nobuild, copyright, git_shorthash_from_head, update_feed, + update_interval +) +from constants import GITVERSION_FILE + +if sys.version_info[0:2] != (3, 10): + raise AssertionError(f'Unexpected python version {sys.version}') + +########################################################################### +# Retrieve current git short hash and store in file GITVERSION_FILE +git_shorthash = git_shorthash_from_head() +if git_shorthash is None: + exit(-1) + +with open(GITVERSION_FILE, 'w+', encoding='utf-8') as gvf: + gvf.write(git_shorthash) + +print(f'Git short hash: {git_shorthash}') +########################################################################### + +if sys.platform == 'win32': + assert platform.architecture()[0] == '32bit', 'Assumes a Python built for 32bit' + import py2exe # noqa: F401 # Yes, this *is* used + dist_dir = 'dist.win32' + +elif sys.platform == 'darwin': + dist_dir = 'dist.macosx' + +else: + assert False, f'Unsupported platform {sys.platform}' + +# Split version, as py2exe wants the 'base' for version +semver = appversion() +appversion_str = str(semver) +base_appversion = str(semver.truncate('patch')) + +if dist_dir and len(dist_dir) > 1 and isdir(dist_dir): + shutil.rmtree(dist_dir) + +# "Developer ID Application" name for signing +macdeveloperid = None + +# Windows paths +WIXPATH = r'C:\Program Files (x86)\WiX Toolset v3.11\bin' +SDKPATH = r'C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x86' + +# OSX paths +SPARKLE = '/Library/Frameworks/Sparkle.framework' + +if sys.platform == 'darwin': + # Patch py2app recipe enumerator to skip the sip recipe since it's too + # enthusiastic - we'll list additional Qt modules explicitly + import py2app.build_app + from py2app import recipes + + # NB: 'Any' is because I don't have MacOS docs + def iter_recipes(module=recipes) -> Generator[str, Any]: + """Enumerate recipes via alternate method.""" + for name in dir(module): + if name.startswith('_') or name == 'sip': + continue + check = getattr(getattr(module, name), 'check', None) + if check is not None: + yield (name, check) + + py2app.build_app.iterRecipes = iter_recipes + + +APP = 'EDMarketConnector.py' +APPCMD = 'EDMC.py' +PLUGINS = [ + 'plugins/coriolis.py', + 'plugins/eddb.py', + 'plugins/eddn.py', + 'plugins/edsm.py', + 'plugins/edsy.py', + 'plugins/inara.py', +] + +if sys.platform == 'darwin': + def get_cfbundle_localizations() -> Set: + """ + Build a set of the localisation files. + + See https://github.com/sparkle-project/Sparkle/issues/238 + """ + return sorted( + ( + [x[:-len('.lproj')] for x in os.listdir(join(SPARKLE, 'Resources')) if x.endswith('.lproj')] + ) | ( + [x[:-len('.strings')] for x in os.listdir('L10n') if x.endswith('.strings')] + ) + ) + + OPTIONS = { + 'py2app': { + 'dist_dir': dist_dir, + 'optimize': 2, + 'packages': [ + 'requests', + 'sqlite3', # Included for plugins + ], + 'includes': [ + 'shutil', # Included for plugins + 'zipfile', # Included for plugins + ], + 'frameworks': [ + 'Sparkle.framework' + ], + 'excludes': [ + 'distutils', + '_markerlib', + 'PIL', + 'pkg_resources', + 'simplejson', + 'unittest' + ], + 'iconfile': f'{appname}.icns', + 'include_plugins': [ + ('plugins', x) for x in PLUGINS + ], + 'resources': [ + '.gitversion', # Contains git short hash + 'ChangeLog.md', + 'snd_good.wav', + 'snd_bad.wav', + 'modules.p', + 'ships.p', + ('FDevIDs', [ + join('FDevIDs', 'commodity.csv'), + join('FDevIDs', 'rare_commodity.csv'), + ]), + ], + 'site_packages': False, + 'plist': { + 'CFBundleName': applongname, + 'CFBundleIdentifier': f'uk.org.marginal.{appname.lower()}', + 'CFBundleLocalizations': get_cfbundle_localizations(), + 'CFBundleShortVersionString': appversion_str, + 'CFBundleVersion': appversion_str, + 'CFBundleURLTypes': [ + { + 'CFBundleTypeRole': 'Viewer', + 'CFBundleURLName': f'uk.org.marginal.{appname.lower()}.URLScheme', + 'CFBundleURLSchemes': [ + 'edmc' + ], + } + ], + 'LSMinimumSystemVersion': '10.10', + 'NSAppleScriptEnabled': True, + 'NSHumanReadableCopyright': copyright, + 'SUEnableAutomaticChecks': True, + 'SUShowReleaseNotes': True, + 'SUAllowsAutomaticUpdates': False, + 'SUFeedURL': update_feed, + 'SUScheduledCheckInterval': update_interval, + }, + 'graph': True, # output dependency graph in dist + } + } + DATA_FILES = [] + +elif 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' + ], + } + } + + 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), + ] + +freeze( + windows=[ + { + 'dest_base': appname, + 'script': APP, + 'icon_resources': [(0, f'{appname}.ico')], + 'company_name': 'EDCD', # Used by WinSparkle + 'product_name': appname, # Used by WinSparkle + 'version': base_appversion, + 'product_version': appversion_str, + 'copyright': copyright, + 'other_resources': [(24, 1, open(f'{appname}.manifest').read())], + } + ], + console=[ + { + 'dest_base': appcmdname, + 'script': APPCMD, + 'company_name': 'EDCD', + 'product_name': appname, + 'version': base_appversion, + 'product_version': appversion_str, + 'copyright': copyright, + 'other_resources': [(24, 1, open(f'{appcmdname}.manifest').read())], + } + ], + data_files=DATA_FILES, + options=OPTIONS, +) + +package_filename = None +if sys.platform == 'darwin': + if isdir(f'{dist_dir}/{applongname}.app'): # from CFBundleName + os.rename(f'{dist_dir}/{applongname}.app', f'{dist_dir}/{appname}.app') + + # Generate OSX-style localization files + for x in os.listdir('L10n'): + if x.endswith('.strings'): + lang = x[:-len('.strings')] + path = f'{dist_dir}/{appname}.app/Contents/Resources/{lang}.lproj' + os.mkdir(path) + codecs.open( + f'{path}/Localizable.strings', + 'w', + 'utf-16' + ).write(codecs.open(f'L10n/{x}', 'r', 'utf-8').read()) + + if macdeveloperid: + os.system(f'codesign --deep -v -s "Developer ID Application: {macdeveloperid}" {dist_dir}/{appname}.app') + + # Make zip for distribution, preserving signature + package_filename = f'{appname}_mac_{appversion_nobuild()}.zip' + os.system(f'cd {dist_dir}; ditto -ck --keepParent --sequesterRsrc {appname}.app ../{package_filename}; cd ..') + +elif sys.platform == 'win32': + 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}') + + 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"') + + 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': 'RegistryEntries' + }, + 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 + ) + + 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') From 05d6160427aa06d12c86bd2244e813fcf83bbd7a Mon Sep 17 00:00:00 2001 From: Athanasius Date: Tue, 6 Sep 2022 17:58:11 +0100 Subject: [PATCH 2/8] Rename py2exe-build.py to Build-exe-and-msi.py This name is more indicative of the functionality. --- py2exe-build.py => Build-exe-and-msi.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename py2exe-build.py => Build-exe-and-msi.py (100%) diff --git a/py2exe-build.py b/Build-exe-and-msi.py similarity index 100% rename from py2exe-build.py rename to Build-exe-and-msi.py From 8eaf1ec70fa43ce14ae9293df27c731cc47577e7 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 23 Sep 2022 11:20:01 +0100 Subject: [PATCH 3/8] windows-build: Switch github workflow to freeze method --- .github/workflows/windows-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index 122ffd6d..dc0fc120 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -38,7 +38,7 @@ jobs: - name: Build EDMC run: | - python setup.py py2exe + python Build-exe-and-msi.py - name: Upload build files uses: actions/upload-artifact@v3 From f4f0c77821c190aa1f2ea4acb5930f8561272761 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 23 Sep 2022 11:25:55 +0100 Subject: [PATCH 4/8] docs/Releasing: Update for using `Build-exe-and-msi.py` --- docs/Releasing.md | 48 ++++++++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/docs/Releasing.md b/docs/Releasing.md index f691ee69..ff317a9f 100644 --- a/docs/Releasing.md +++ b/docs/Releasing.md @@ -61,8 +61,9 @@ You will need several pieces of software installed, or the files from their If you are using different versions of any of these tools then please ensure that the paths where they're installed match the associated lines in -`setup.py`. i.e. if you're using later WiX you might need to edit the WIXPATH -line, and likewise the SDKPATH line if you're using a later Windows SDK kit. +`Build-exe-and-msi.py`. i.e. if you're using later WiX you might need to edit +the WIXPATH line, and likewise the SDKPATH line if you're using a later +Windows SDK kit. # Version Strings @@ -99,13 +100,13 @@ resulting .exe and/or .msi files. **But** realise that the resulting program will still try to check for new versions at the main URL unless you change that. -1. Company is set in `setup.py`. Search for `company_name`. This is what - appears in the EXE properties, and is also used as the location of WinSparkle - registry entries on Windows. +1. Company is set in `Build-exe-and-msi.py`. Search for `company_name`. This + is what appears in the EXE properties, and is also used as the location of + WinSparkle registry entries on Windows. 1. Application names, version and URL of the file with latest release information. These are all in the `config/__init__.py` file. See the - `from config import ...` lines in setup.py. + `from config import ...` lines in `Build-exe-and-msi.py`: 1. `appname`: The short appname, e.g. 'EDMarketConnector' 2. `applongname`: The long appname, e.g. 'E:D Market Connector' 3. `appcmdname`: The CLI appname, e.g. 'EDMC' @@ -143,10 +144,10 @@ that. If you add a new file to the program that needs to be distributed to users as well then you will need to properly add it to the build process. -### setup.py +### Build-exe-and-msi.py -You'll need to add it in setup.py so that py2exe includes it in the build. -Add the file to the DATA_FILES statement. +You'll need to add it in `Build-exe-and-msi.py` so that py2exe includes it in +the build. Add the file to the DATA_FILES statement. ### WiX @@ -257,38 +258,46 @@ a 'Git bash' window. The 'Terminal' tab of PyCharm works fine. Assuming the correct python.exe is associated with .py files then simply run: ```batch -setup.py py2exe +Build-exe-and-msi.py ``` else you might need this, which assumes correct python.exe is in your PATH: ```batch -python.exe setup.py py2exe +python.exe Build-exe-and-msi.py ``` else you'll have to specify the path to python.exe, e.g.: ```batch -"C:\Program Files \(x86)\Python38-32\python.exe" setup.py py2exe +"C:\Program Files \(x86)\Python38-32\python.exe" Build-exe-and-msi.py ``` Output will be something like (`...` denoting parts elided for brevity): ```plaintext -running py2exe +Git short hash: 993f946b.DIRTY +INFO:runtime:Analyzing the code +INFO:runtime:Found 695 modules, 60 are missing, 0 may be missing ... Building 'dist.win32\EDMC.exe'. Building 'dist.win32\EDMarketConnector.exe'. -Building shared code archive 'dist.win32\library.zip'. ... -Windows Installer XML Toolset Compiler version 3.11.1.2318 +Windows Installer XML Toolset Toolset Harvester version 3.11.2.4516 +Copyright (c) .NET Foundation and contributors. All rights reserved. + +Windows Installer XML Toolset Compiler version 3.11.2.4516 +Copyright (c) .NET Foundation and contributors. All rights reserved. + +EDMarketConnector.wxs +Windows Installer XML Toolset Linker version 3.11.2.4516 Copyright (c) .NET Foundation and contributors. All rights reserved. ... -Package language = 1033,1029,1031,1034,1035,1036,1038,1040,1041,1043,1045,1046,1049,1058,1062,2052,2070,2074,0, ProductLanguage = 1029, Database codepage = 0 +Package language = 1033,1029,1031,1034,1035,1036,1038,1040,1041,1043,1045,1046,1049,1058,1062,2052,2070,2074,6170,1060,1053,18,0, ProductLanguage = 1029, Database codepage = 0 MsiTran V 5.0 Copyright (c) Microsoft Corporation. All Rights Reserved ... -DonePackage language = 1033,1029,1031,1034,1035,1036,1038,1040,1041,1043,1045,1046,1049,1058,1062,2052,2070,2074,0, ProductLanguage = 0, Database codepage = 0 +DonePackage language = 1033,1029,1031,1034,1035,1036,1038,1040,1041,1043,1045,1046,1049,1058,1062,2052,2070,2074,6170,1060,1053,18,0, ProductLanguage = 0, Database codepage = 0 MsiTran V 5.0 Copyright (c) Microsoft Corporation. All Rights Reserved @@ -297,7 +306,8 @@ Done **Do check the output** for things like not properly specifying extra files to be included in the install. If they're not picked up by current rules in -`setup.py` then you will need to add them to the `win32` `DATA_FILES` array. +`Build-exe-and-msi.py` then you will need to add them to the `win32` +`DATA_FILES` array. You should now have one new/updated folder `dist.win32` and two new files (version string dependent): `EDMarketConnector_win_4.0.2.msi` and @@ -444,7 +454,7 @@ When changing the Python version (Major.Minor.Patch) used: 1. Major or Minor level changes: - 1. `setup.py` will need its version check updating. + 1. `Build-exe-and-msi.py` will need its version check updating. 2. `EDMarketConnector.wxs` will need updating to reference the correct pythonXX.dll file. 3. `.pre-commit-config.yaml` will need the `default_language_version` From a9ef7360673127c69e680abe2e18da0f9afb3b06 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 23 Sep 2022 11:32:39 +0100 Subject: [PATCH 5/8] Build: Use `version_info` for DRYness * Adds `language` so that WiX is happy. Hard-coded to "English (United States)". * Also adds `description` (same as the GitHub repo). --- Build-exe-and-msi.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Build-exe-and-msi.py b/Build-exe-and-msi.py index e54489ef..7f068d97 100644 --- a/Build-exe-and-msi.py +++ b/Build-exe-and-msi.py @@ -232,16 +232,21 @@ elif sys.platform == 'win32': ] freeze( + version_info={ + 'description': 'Downloads commodity market and other station data from the game Elite Dangerous for use with' + ' all popular online and offline trading tools.', + 'company_name': 'EDCD', # Used by WinSparkle + 'product_name': appname, # Used by WinSparkle + 'version': base_appversion, + 'product_version': appversion_str, + 'copyright': copyright, + 'language': 'English (United States)', + }, windows=[ { 'dest_base': appname, 'script': APP, 'icon_resources': [(0, f'{appname}.ico')], - 'company_name': 'EDCD', # Used by WinSparkle - 'product_name': appname, # Used by WinSparkle - 'version': base_appversion, - 'product_version': appversion_str, - 'copyright': copyright, 'other_resources': [(24, 1, open(f'{appname}.manifest').read())], } ], @@ -249,11 +254,6 @@ freeze( { 'dest_base': appcmdname, 'script': APPCMD, - 'company_name': 'EDCD', - 'product_name': appname, - 'version': base_appversion, - 'product_version': appversion_str, - 'copyright': copyright, 'other_resources': [(24, 1, open(f'{appcmdname}.manifest').read())], } ], From a3198c5e360e570cc762a552535a1d8512dd49cc Mon Sep 17 00:00:00 2001 From: Athanasius Date: Fri, 23 Sep 2022 11:42:18 +0100 Subject: [PATCH 6/8] Build: Remove all of the macOS code & comment out sections * We've not been building for macOS since the change of maintainership in 2020, so removing that code is long overdue. However we're leaving in the test for `win32` so as to be explicit that this should only (currently) be run on that platform. * Add section comments to better document what each code section is meant to achieve. --- Build-exe-and-msi.py | 189 +++++++++---------------------------------- 1 file changed, 39 insertions(+), 150 deletions(-) diff --git a/Build-exe-and-msi.py b/Build-exe-and-msi.py index 7f068d97..ab98d8a1 100644 --- a/Build-exe-and-msi.py +++ b/Build-exe-and-msi.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 """Build to executables and MSI installer using py2exe and other tools.""" -import codecs import os import pathlib import platform @@ -9,22 +8,31 @@ import shutil import sys from os.path import exists, isdir, join from tempfile import gettempdir -from typing import Any, Generator, Set from lxml import etree from py2exe import freeze -from config import ( - appcmdname, applongname, appname, appversion, appversion_nobuild, copyright, git_shorthash_from_head, update_feed, - update_interval -) +from config import appcmdname, appname, appversion, appversion_nobuild, copyright, git_shorthash_from_head from constants import GITVERSION_FILE +########################################################################### +# Check we're on a supported platform +########################################################################### 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' + import py2exe # noqa: F401 # Yes, this *is* used + dist_dir = 'dist.win32' + +else: + assert False, f'Unsupported platform {sys.platform}' +########################################################################### + ########################################################################### # Retrieve current git short hash and store in file GITVERSION_FILE +########################################################################### git_shorthash = git_shorthash_from_head() if git_shorthash is None: exit(-1) @@ -35,54 +43,28 @@ with open(GITVERSION_FILE, 'w+', encoding='utf-8') as gvf: print(f'Git short hash: {git_shorthash}') ########################################################################### -if sys.platform == 'win32': - assert platform.architecture()[0] == '32bit', 'Assumes a Python built for 32bit' - import py2exe # noqa: F401 # Yes, this *is* used - dist_dir = 'dist.win32' - -elif sys.platform == 'darwin': - dist_dir = 'dist.macosx' - -else: - assert False, f'Unsupported platform {sys.platform}' - +########################################################################### +# Misc. Configuration +########################################################################### # Split version, as py2exe wants the 'base' for version semver = appversion() appversion_str = str(semver) base_appversion = str(semver.truncate('patch')) +# Ensure a clean `dist_dir` by first removing it. if dist_dir and len(dist_dir) > 1 and isdir(dist_dir): shutil.rmtree(dist_dir) -# "Developer ID Application" name for signing -macdeveloperid = None - # Windows paths WIXPATH = r'C:\Program Files (x86)\WiX Toolset v3.11\bin' SDKPATH = r'C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x86' +########################################################################### -# OSX paths -SPARKLE = '/Library/Frameworks/Sparkle.framework' - -if sys.platform == 'darwin': - # Patch py2app recipe enumerator to skip the sip recipe since it's too - # enthusiastic - we'll list additional Qt modules explicitly - import py2app.build_app - from py2app import recipes - - # NB: 'Any' is because I don't have MacOS docs - def iter_recipes(module=recipes) -> Generator[str, Any]: - """Enumerate recipes via alternate method.""" - for name in dir(module): - if name.startswith('_') or name == 'sip': - continue - check = getattr(getattr(module, name), 'check', None) - if check is not None: - yield (name, check) - - py2app.build_app.iterRecipes = iter_recipes - +########################################################################### +########################################################################### +# Set up all the options, extra files etc. for py2exe build. +########################################################################### APP = 'EDMarketConnector.py' APPCMD = 'EDMC.py' PLUGINS = [ @@ -94,91 +76,8 @@ PLUGINS = [ 'plugins/inara.py', ] -if sys.platform == 'darwin': - def get_cfbundle_localizations() -> Set: - """ - Build a set of the localisation files. - - See https://github.com/sparkle-project/Sparkle/issues/238 - """ - return sorted( - ( - [x[:-len('.lproj')] for x in os.listdir(join(SPARKLE, 'Resources')) if x.endswith('.lproj')] - ) | ( - [x[:-len('.strings')] for x in os.listdir('L10n') if x.endswith('.strings')] - ) - ) - - OPTIONS = { - 'py2app': { - 'dist_dir': dist_dir, - 'optimize': 2, - 'packages': [ - 'requests', - 'sqlite3', # Included for plugins - ], - 'includes': [ - 'shutil', # Included for plugins - 'zipfile', # Included for plugins - ], - 'frameworks': [ - 'Sparkle.framework' - ], - 'excludes': [ - 'distutils', - '_markerlib', - 'PIL', - 'pkg_resources', - 'simplejson', - 'unittest' - ], - 'iconfile': f'{appname}.icns', - 'include_plugins': [ - ('plugins', x) for x in PLUGINS - ], - 'resources': [ - '.gitversion', # Contains git short hash - 'ChangeLog.md', - 'snd_good.wav', - 'snd_bad.wav', - 'modules.p', - 'ships.p', - ('FDevIDs', [ - join('FDevIDs', 'commodity.csv'), - join('FDevIDs', 'rare_commodity.csv'), - ]), - ], - 'site_packages': False, - 'plist': { - 'CFBundleName': applongname, - 'CFBundleIdentifier': f'uk.org.marginal.{appname.lower()}', - 'CFBundleLocalizations': get_cfbundle_localizations(), - 'CFBundleShortVersionString': appversion_str, - 'CFBundleVersion': appversion_str, - 'CFBundleURLTypes': [ - { - 'CFBundleTypeRole': 'Viewer', - 'CFBundleURLName': f'uk.org.marginal.{appname.lower()}.URLScheme', - 'CFBundleURLSchemes': [ - 'edmc' - ], - } - ], - 'LSMinimumSystemVersion': '10.10', - 'NSAppleScriptEnabled': True, - 'NSHumanReadableCopyright': copyright, - 'SUEnableAutomaticChecks': True, - 'SUShowReleaseNotes': True, - 'SUAllowsAutomaticUpdates': False, - 'SUFeedURL': update_feed, - 'SUScheduledCheckInterval': update_interval, - }, - 'graph': True, # output dependency graph in dist - } - } - DATA_FILES = [] - -elif sys.platform == 'win32': +# Ensure this fails on non-win32 +if sys.platform == 'win32': OPTIONS = { 'py2exe': { 'dist_dir': dist_dir, @@ -231,6 +130,13 @@ elif sys.platform == 'win32': ('plugins', PLUGINS), ] +else: + raise AssertionError('Unsupported platform') +########################################################################### + +########################################################################### +# Use py2exe's `freeze()` to produce the executables. +########################################################################### freeze( version_info={ 'description': 'Downloads commodity market and other station data from the game Elite Dangerous for use with' @@ -260,32 +166,14 @@ freeze( data_files=DATA_FILES, options=OPTIONS, ) +########################################################################### +########################################################################### +# Build installer(s) +########################################################################### package_filename = None -if sys.platform == 'darwin': - if isdir(f'{dist_dir}/{applongname}.app'): # from CFBundleName - os.rename(f'{dist_dir}/{applongname}.app', f'{dist_dir}/{appname}.app') - - # Generate OSX-style localization files - for x in os.listdir('L10n'): - if x.endswith('.strings'): - lang = x[:-len('.strings')] - path = f'{dist_dir}/{appname}.app/Contents/Resources/{lang}.lproj' - os.mkdir(path) - codecs.open( - f'{path}/Localizable.strings', - 'w', - 'utf-16' - ).write(codecs.open(f'L10n/{x}', 'r', 'utf-8').read()) - - if macdeveloperid: - os.system(f'codesign --deep -v -s "Developer ID Application: {macdeveloperid}" {dist_dir}/{appname}.app') - - # Make zip for distribution, preserving signature - package_filename = f'{appname}_mac_{appversion_nobuild()}.zip' - os.system(f'cd {dist_dir}; ditto -ck --keepParent --sequesterRsrc {appname}.app ../{package_filename}; cd ..') - -elif sys.platform == 'win32': +# 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') @@ -416,3 +304,4 @@ elif sys.platform == 'win32': else: raise AssertionError('Unsupported platform') +########################################################################### From 9347f12723ca4e15942c3d214a25c497e9ee88f2 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sat, 24 Sep 2022 10:39:38 +0100 Subject: [PATCH 7/8] build: Remove old `setup.py` and `py2exe.cmd` Use the git, Ath. --- py2exe.cmd | 2 - setup.py | 428 ----------------------------------------------------- 2 files changed, 430 deletions(-) delete mode 100755 py2exe.cmd delete mode 100755 setup.py diff --git a/py2exe.cmd b/py2exe.cmd deleted file mode 100755 index b0205282..00000000 --- a/py2exe.cmd +++ /dev/null @@ -1,2 +0,0 @@ -@REM http://www.py2exe.org/index.cgi/OptimizedBytecode -"C:\Program Files (x86)\Python37-32\python.exe" -OO setup.py py2exe diff --git a/setup.py b/setup.py deleted file mode 100755 index c078e225..00000000 --- a/setup.py +++ /dev/null @@ -1,428 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Script to build to .exe and .msi package. - -.exe build is via py2exe on win32. -.msi packaging utilises Windows SDK. -""" - -import codecs -import os -import pathlib -import platform -import re -import shutil -import sys -from distutils.core import setup -from os.path import exists, isdir, join -from tempfile import gettempdir -from typing import Any, Generator, Set - -from lxml import etree - -from config import ( - appcmdname, applongname, appname, appversion, appversion_nobuild, copyright, git_shorthash_from_head, update_feed, - update_interval -) -from constants import GITVERSION_FILE - -if sys.version_info[0:2] != (3, 10): - raise AssertionError(f'Unexpected python version {sys.version}') - -########################################################################### -# Retrieve current git short hash and store in file GITVERSION_FILE -git_shorthash = git_shorthash_from_head() -if git_shorthash is None: - exit(-1) - -with open(GITVERSION_FILE, 'w+', encoding='utf-8') as gvf: - gvf.write(git_shorthash) - -print(f'Git short hash: {git_shorthash}') -########################################################################### - -if sys.platform == 'win32': - assert platform.architecture()[0] == '32bit', 'Assumes a Python built for 32bit' - import py2exe # noqa: F401 # Yes, this *is* used - dist_dir = 'dist.win32' - -elif sys.platform == 'darwin': - dist_dir = 'dist.macosx' - -else: - assert False, f'Unsupported platform {sys.platform}' - -# Split version, as py2exe wants the 'base' for version -semver = appversion() -appversion_str = str(semver) -base_appversion = str(semver.truncate('patch')) - -if dist_dir and len(dist_dir) > 1 and isdir(dist_dir): - shutil.rmtree(dist_dir) - -# "Developer ID Application" name for signing -macdeveloperid = None - -# Windows paths -WIXPATH = r'C:\Program Files (x86)\WiX Toolset v3.11\bin' -SDKPATH = r'C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x86' - -# OSX paths -SPARKLE = '/Library/Frameworks/Sparkle.framework' - -if sys.platform == 'darwin': - # Patch py2app recipe enumerator to skip the sip recipe since it's too - # enthusiastic - we'll list additional Qt modules explicitly - import py2app.build_app - from py2app import recipes - - # NB: 'Any' is because I don't have MacOS docs - def iter_recipes(module=recipes) -> Generator[str, Any]: - """Enumerate recipes via alternate method.""" - for name in dir(module): - if name.startswith('_') or name == 'sip': - continue - check = getattr(getattr(module, name), 'check', None) - if check is not None: - yield (name, check) - - py2app.build_app.iterRecipes = iter_recipes - - -APP = 'EDMarketConnector.py' -APPCMD = 'EDMC.py' -PLUGINS = [ - 'plugins/coriolis.py', - 'plugins/eddb.py', - 'plugins/eddn.py', - 'plugins/edsm.py', - 'plugins/edsy.py', - 'plugins/inara.py', -] - -if sys.platform == 'darwin': - def get_cfbundle_localizations() -> Set: - """ - Build a set of the localisation files. - - See https://github.com/sparkle-project/Sparkle/issues/238 - """ - return sorted( - ( - [x[:-len('.lproj')] for x in os.listdir(join(SPARKLE, 'Resources')) if x.endswith('.lproj')] - ) | ( - [x[:-len('.strings')] for x in os.listdir('L10n') if x.endswith('.strings')] - ) - ) - - OPTIONS = { - 'py2app': { - 'dist_dir': dist_dir, - 'optimize': 2, - 'packages': [ - 'requests', - 'sqlite3', # Included for plugins - ], - 'includes': [ - 'shutil', # Included for plugins - 'zipfile', # Included for plugins - ], - 'frameworks': [ - 'Sparkle.framework' - ], - 'excludes': [ - 'distutils', - '_markerlib', - 'PIL', - 'pkg_resources', - 'simplejson', - 'unittest' - ], - 'iconfile': f'{appname}.icns', - 'include_plugins': [ - ('plugins', x) for x in PLUGINS - ], - 'resources': [ - '.gitversion', # Contains git short hash - 'ChangeLog.md', - 'snd_good.wav', - 'snd_bad.wav', - 'modules.p', - 'ships.p', - ('FDevIDs', [ - join('FDevIDs', 'commodity.csv'), - join('FDevIDs', 'rare_commodity.csv'), - ]), - ], - 'site_packages': False, - 'plist': { - 'CFBundleName': applongname, - 'CFBundleIdentifier': f'uk.org.marginal.{appname.lower()}', - 'CFBundleLocalizations': get_cfbundle_localizations(), - 'CFBundleShortVersionString': appversion_str, - 'CFBundleVersion': appversion_str, - 'CFBundleURLTypes': [ - { - 'CFBundleTypeRole': 'Viewer', - 'CFBundleURLName': f'uk.org.marginal.{appname.lower()}.URLScheme', - 'CFBundleURLSchemes': [ - 'edmc' - ], - } - ], - 'LSMinimumSystemVersion': '10.10', - 'NSAppleScriptEnabled': True, - 'NSHumanReadableCopyright': copyright, - 'SUEnableAutomaticChecks': True, - 'SUShowReleaseNotes': True, - 'SUAllowsAutomaticUpdates': False, - 'SUFeedURL': update_feed, - 'SUScheduledCheckInterval': update_interval, - }, - 'graph': True, # output dependency graph in dist - } - } - DATA_FILES = [] - -elif 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' - ], - } - } - - 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), - ] - -setup( - name=applongname, - version=appversion_str, - windows=[ - { - 'dest_base': appname, - 'script': APP, - 'icon_resources': [(0, f'{appname}.ico')], - 'company_name': 'EDCD', # Used by WinSparkle - 'product_name': appname, # Used by WinSparkle - 'version': base_appversion, - 'product_version': appversion_str, - 'copyright': copyright, - 'other_resources': [(24, 1, open(f'{appname}.manifest').read())], - } - ], - console=[ - { - 'dest_base': appcmdname, - 'script': APPCMD, - 'company_name': 'EDCD', - 'product_name': appname, - 'version': base_appversion, - 'product_version': appversion_str, - 'copyright': copyright, - 'other_resources': [(24, 1, open(f'{appcmdname}.manifest').read())], - } - ], - data_files=DATA_FILES, - options=OPTIONS, - py_modules=[], -) - -package_filename = None -if sys.platform == 'darwin': - if isdir(f'{dist_dir}/{applongname}.app'): # from CFBundleName - os.rename(f'{dist_dir}/{applongname}.app', f'{dist_dir}/{appname}.app') - - # Generate OSX-style localization files - for x in os.listdir('L10n'): - if x.endswith('.strings'): - lang = x[:-len('.strings')] - path = f'{dist_dir}/{appname}.app/Contents/Resources/{lang}.lproj' - os.mkdir(path) - codecs.open( - f'{path}/Localizable.strings', - 'w', - 'utf-16' - ).write(codecs.open(f'L10n/{x}', 'r', 'utf-8').read()) - - if macdeveloperid: - os.system(f'codesign --deep -v -s "Developer ID Application: {macdeveloperid}" {dist_dir}/{appname}.app') - - # Make zip for distribution, preserving signature - package_filename = f'{appname}_mac_{appversion_nobuild()}.zip' - os.system(f'cd {dist_dir}; ditto -ck --keepParent --sequesterRsrc {appname}.app ../{package_filename}; cd ..') - -elif sys.platform == 'win32': - 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}') - - 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"') - - 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': 'RegistryEntries' - }, - 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 - ) - - 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') From 26c2bd720f21ddb90f9ea3ec59dad6855cd4a2f4 Mon Sep 17 00:00:00 2001 From: Athanasius Date: Sat, 24 Sep 2022 10:59:44 +0100 Subject: [PATCH 8/8] 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}') ###########################################################################