diff --git a/pkgutils.py b/pkgutils.py index 1110b287..3467a156 100755 --- a/pkgutils.py +++ b/pkgutils.py @@ -37,9 +37,10 @@ from pathlib import Path import utils.binary_dist import utils.icon_themes +import utils.windows_build + +from utils.common import ROOT_DIR, SETUP_DIR, copySourceCode, extractVersion, readFile, writeFile -CURR_DIR = Path(__file__).parent -SETUP_DIR = CURR_DIR / "setup" SIGN_KEY = "D6A9F6B8F227CF7C6F6D1EE84DBBE4B734B0BD08" OS_LINUX = sys.platform.startswith("linux") @@ -51,36 +52,6 @@ OS_WIN = sys.platform.startswith("win32") # Utilities # =============================================================================================== # -def extractVersion(beQuiet: bool = False) -> tuple[str, str, str]: - """Extract the novelWriter version number without having to import - anything else from the main package. - """ - def getValue(text: str) -> str: - bits = text.partition("=") - return bits[2].strip().strip('"') - - numVers = "0" - hexVers = "0x0" - relDate = "Unknown" - initFile = Path("novelwriter") / "__init__.py" - try: - for aLine in initFile.read_text(encoding="utf-8").splitlines(): - if aLine.startswith("__version__"): - numVers = getValue((aLine)) - if aLine.startswith("__hexversion__"): - hexVers = getValue((aLine)) - if aLine.startswith("__date__"): - relDate = getValue((aLine)) - except Exception as exc: - print("Could not read file: %s" % initFile) - print(str(exc)) - - if not beQuiet: - print("novelWriter version: %s (%s) at %s" % (numVers, hexVers, relDate)) - - return numVers, hexVers, relDate - - def stripVersion(version: str) -> str: """Strip the pre-release part from a version number.""" if "a" in version: @@ -93,16 +64,6 @@ def stripVersion(version: str) -> str: return version -def readFile(file: Path) -> str: - """Read an entire file and return as a string.""" - return file.read_text(encoding="utf-8") - - -def writeFile(file: Path, text: str) -> int: - """Write string to file.""" - return file.write_text(text, encoding="utf-8") - - def toUpload(srcPath: str | Path, dstName: str | None = None) -> None: """Copy a file produced by one of the build functions to the upload directory. The file can optionally be given a new name. @@ -138,17 +99,17 @@ def checkAssetsExist() -> bool: hasManual = False hasQmData = False - sampleZip = CURR_DIR / "novelwriter" / "assets" / "sample.zip" + sampleZip = ROOT_DIR / "novelwriter" / "assets" / "sample.zip" if sampleZip.is_file(): print(f"Found: {sampleZip}") hasSample = True - pdfManual = CURR_DIR / "novelwriter" / "assets" / "manual.pdf" + pdfManual = ROOT_DIR / "novelwriter" / "assets" / "manual.pdf" if pdfManual.is_file(): print(f"Found: {pdfManual}") hasManual = True - i18nAssets = CURR_DIR / "novelwriter" / "assets" / "i18n" + i18nAssets = ROOT_DIR / "novelwriter" / "assets" / "i18n" if len(list(i18nAssets.glob("*.qm"))) > 0: print(f"Found: {i18nAssets}/*.qm") hasQmData = True @@ -214,12 +175,12 @@ def cleanBuildDirs(args: argparse.Namespace) -> None: print("") folders = [ - CURR_DIR / "build", - CURR_DIR / "dist", - CURR_DIR / "dist_deb", - CURR_DIR / "dist_minimal", - CURR_DIR / "dist_appimage", - CURR_DIR / "novelWriter.egg-info", + ROOT_DIR / "build", + ROOT_DIR / "dist", + ROOT_DIR / "dist_deb", + ROOT_DIR / "dist_minimal", + ROOT_DIR / "dist_appimage", + ROOT_DIR / "novelWriter.egg-info", ] for folder in folders: @@ -252,8 +213,8 @@ def buildPdfManual(args: argparse.Namespace | None = None) -> None: print("===================") print("") - buildFile = CURR_DIR / "docs" / "build" / "latex" / "manual.pdf" - finalFile = CURR_DIR / "novelwriter" / "assets" / "manual.pdf" + buildFile = ROOT_DIR / "docs" / "build" / "latex" / "manual.pdf" + finalFile = ROOT_DIR / "novelwriter" / "assets" / "manual.pdf" finalFile.unlink(missing_ok=True) try: @@ -303,8 +264,8 @@ def buildSampleZip(args: argparse.Namespace | None = None) -> None: print("========================") print("") - srcSample = CURR_DIR / "sample" - dstSample = CURR_DIR / "novelwriter" / "assets" / "sample.zip" + srcSample = ROOT_DIR / "sample" + dstSample = ROOT_DIR / "novelwriter" / "assets" / "sample.zip" if srcSample.is_dir(): dstSample.unlink(missing_ok=True) @@ -342,8 +303,8 @@ def importI18nUpdates(args: argparse.Namespace) -> None: print("File not found ...") sys.exit(1) - dstPath = CURR_DIR / "novelwriter" / "assets" / "i18n" - srcPath = CURR_DIR / "i18n" + dstPath = ROOT_DIR / "novelwriter" / "assets" / "i18n" + srcPath = ROOT_DIR / "i18n" print(f"Loading file: {fileName}") with zipfile.ZipFile(fileName) as zipObj: @@ -384,10 +345,10 @@ def updateTranslationSources(args: argparse.Namespace) -> None: print("Scanning Source Tree:") print("") - sources = list((CURR_DIR / "novelwriter").glob("**/*.py")) - sources.insert(0, CURR_DIR / "i18n" / "qtbase.py") + sources = list((ROOT_DIR / "novelwriter").glob("**/*.py")) + sources.insert(0, ROOT_DIR / "i18n" / "qtbase.py") for source in sources: - print(source.relative_to(CURR_DIR)) + print(source.relative_to(ROOT_DIR)) print("") print("TS Files to Update:") @@ -444,8 +405,8 @@ def buildTranslationAssets(args: argparse.Namespace | None = None) -> None: print("TS Files to Build:") print("") - srcDir = CURR_DIR / "i18n" - dstDir = CURR_DIR / "novelwriter" / "assets" / "i18n" + srcDir = ROOT_DIR / "i18n" + dstDir = ROOT_DIR / "novelwriter" / "assets" / "i18n" srcList = [] for item in srcDir.iterdir(): @@ -469,11 +430,11 @@ def buildTranslationAssets(args: argparse.Namespace | None = None) -> None: print("Moving QM Files to Assets") print("") - dstRel = dstDir.relative_to(CURR_DIR) + dstRel = dstDir.relative_to(ROOT_DIR) for item in srcDir.iterdir(): if item.is_file() and item.suffix == ".qm": item.rename(dstDir / item.name) - print("Moved: %s -> %s" % (item.relative_to(CURR_DIR), dstRel / item.name)) + print("Moved: %s -> %s" % (item.relative_to(ROOT_DIR), dstRel / item.name)) print("") @@ -492,14 +453,14 @@ def cleanBuiltAssets(args: argparse.Namespace | None = None) -> None: print("") assets = [ - CURR_DIR / "novelwriter" / "assets" / "sample.zip", - CURR_DIR / "novelwriter" / "assets" / "manual.pdf", + ROOT_DIR / "novelwriter" / "assets" / "sample.zip", + ROOT_DIR / "novelwriter" / "assets" / "manual.pdf", ] - assets.extend((CURR_DIR / "novelwriter" / "assets" / "i18n").glob("*.qm")) + assets.extend((ROOT_DIR / "novelwriter" / "assets" / "i18n").glob("*.qm")) for asset in assets: if asset.is_file(): asset.unlink() - print(f"Deleted: {asset.relative_to(CURR_DIR)}") + print(f"Deleted: {asset.relative_to(ROOT_DIR)}") print("") @@ -523,29 +484,6 @@ def buildAllAssets(args: argparse.Namespace) -> None: # Python Packaging # =============================================================================================== # -## -# Copy Source -## - -def copySourceCode(dst: Path) -> None: - """Copy the novelwriter source tree to path.""" - src = CURR_DIR / "novelwriter" - for item in src.glob("**/*"): - relSrc = item.relative_to(CURR_DIR) - if item.suffix in (".pyc", ".pyo"): - print(f"Ignore: {relSrc}") - continue - if item.parent.is_dir() and item.parent.name != "__pycache__": - dstDir = dst / relSrc.parent - if not dstDir.exists(): - dstDir.mkdir(parents=True) - print(f"Folder: {dstDir}") - if item.is_file(): - shutil.copyfile(item, dst / relSrc) - print(f"Copied: {dst / relSrc}") - return - - ## # Copy Package Files ## @@ -572,7 +510,7 @@ def copyPackageFiles(dst: Path, setupPy: bool = False) -> None: )) print("Wrote: setup.py") - text = readFile(CURR_DIR / "pyproject.toml") + text = readFile(ROOT_DIR / "pyproject.toml") text = text.replace("setup/description_pypi.md", "data/description_short.txt") writeFile(dst / "pyproject.toml", text) print("Wrote: pyproject.toml") @@ -613,7 +551,7 @@ def makeDebianPackage( # Set Up Folder # ============= - bldDir = CURR_DIR / "dist_deb" + bldDir = ROOT_DIR / "dist_deb" bldPkg = f"novelwriter_{pkgVers}" outDir = bldDir / bldPkg debDir = outDir / "debian" @@ -814,7 +752,7 @@ def buildAppImage(args: argparse.Namespace) -> None: # Set Up Folder # ============= - bldDir = CURR_DIR / "dist_appimage" + bldDir = ROOT_DIR / "dist_appimage" bldPkg = f"novelwriter_{pkgVers}" outDir = bldDir / bldPkg imgDir = bldDir / "appimage" @@ -915,217 +853,6 @@ def buildAppImage(args: argparse.Namespace) -> None: return -## -# Make Windows Setup EXE (build-win-exe) -## - -def makeWindowsEmbedded(args: argparse.Namespace) -> None: - """Set up a package with embedded Python and dependencies for - Windows installation. - """ - import compileall - import urllib.request - import zipfile - - print("") - print("Build Standalone Windows Package") - print("================================") - print("") - - numVers, _, _ = extractVersion() - print("Version: %s" % numVers) - - # Set Up Folder - # ============= - - bldDir = CURR_DIR / "dist" - outDir = bldDir / "novelWriter" - libDir = outDir / "lib" - if outDir.exists(): - shutil.rmtree(outDir) - - bldDir.mkdir(exist_ok=True) - outDir.mkdir() - libDir.mkdir() - - # Copy novelWriter Source - # ======================= - - print("Copying and compiling novelWriter source ...") - print("") - - copySourceCode(outDir) - - files = [ - CURR_DIR / "CREDITS.md", - CURR_DIR / "LICENSE.md", - CURR_DIR / "requirements.txt", - SETUP_DIR / "icons" / "novelwriter.ico", - SETUP_DIR / "iss_license.txt", - - ] - for item in files: - shutil.copyfile(item, outDir / item.name) - print(f"Copied: {item} > {outDir / item.name}") - - compileall.compile_dir(outDir / "novelwriter") - - print("Done") - print("") - - # Download Python Embeddable - # ========================== - - print("Adding Python embeddable ...") - - pyVers = "%d.%d.%d" % (sys.version_info[:3]) - zipFile = f"python-{pyVers}-embed-amd64.zip" - pyZip = bldDir / zipFile - if not pyZip.is_file(): - pyUrl = f"https://www.python.org/ftp/python/{pyVers}/{zipFile}" - print("Downloading: %s" % pyUrl) - urllib.request.urlretrieve(pyUrl, pyZip) - - print("Extracting ...") - with zipfile.ZipFile(pyZip, "r") as inFile: - inFile.extractall(outDir) - - print("Done") - print("") - - # Install Dependencies - # ==================== - - print("Install dependencies ...") - - try: - subprocess.call([ - sys.executable, "-m", - "pip", "install", "-r", "requirements.txt", "--target", str(libDir) - ]) - except Exception as exc: - print("Failed with error:") - print(str(exc)) - sys.exit(1) - - print("Done") - print("") - - # Update Launch File - # ================== - - print("Updating starting script ...") - - writeFile(outDir / "novelWriter.pyw", ( - "#!/usr/bin/env python3\n" - "import os\n" - "import sys\n" - "\n" - "os.curdir = os.path.abspath(os.path.dirname(__file__))\n" - "sys.path.insert(0, os.path.join(os.curdir, \"lib\"))\n" - "\n" - "if __name__ == \"__main__\":\n" - " import novelwriter\n" - " novelwriter.main(sys.argv[1:])\n" - )) - - print("Done") - print("") - - # Clean Up Files - # ============== - - def unlinkIfFound(file: Path) -> None: - if file.is_file(): - file.unlink() - print(f"Deleted: {file}") - - def deleteFolder(folder: Path) -> None: - if folder.is_dir(): - shutil.rmtree(folder) - print(f"Deleted: {folder}") - - print("Deleting Redundant Files") - print("========================") - print("") - - pyQt5Dir = libDir / "PyQt5" - bindDir = pyQt5Dir / "bindings" - qt5Dir = pyQt5Dir / "Qt5" - binDir = qt5Dir / "bin" - plugDir = qt5Dir / "plugins" - qmDir = qt5Dir / "translations" - dictDir = libDir / "enchant" / "data" / "mingw64" / "share" / "enchant" / "hunspell" - - for item in dictDir.iterdir(): - if not item.name.startswith(("en_GB", "en_US")): - unlinkIfFound(item) - - for item in qmDir.iterdir(): - if not item.name.startswith("qtbase"): - unlinkIfFound(item) - - delQt5 = [ - "Qt5Bluetooth", "Qt5DBus", "Qt5Designer", "Qt5Designer", "Qt5Help", "Qt5Location", - "Qt5Multimedia", "Qt5MultimediaWidgets", "Qt5Network", "Qt5Nfc", "Qt5OpenGL", - "Qt5Positioning", "Qt5PositioningQuick", "Qt5Qml", "Qt5QmlModels", "Qt5QmlWorkerScript", - "Qt5Quick", "Qt5Quick3D", "Qt5Quick3DAssetImport", "Qt5Quick3DRender", - "Qt5Quick3DRuntimeRender", "Qt5Quick3DUtils", "Qt5QuickControls2", "Qt5QuickParticles", - "Qt5QuickShapes", "Qt5QuickTemplates2", "Qt5QuickTest", "Qt5QuickWidgets", "Qt5Sensors", - "Qt5SerialPort", "Qt5Sql", "Qt5Test", "Qt5TextToSpeech", "Qt5WebChannel", "Qt5WebSockets", - "Qt5WebView", "Qt5Xml", "Qt5XmlPatterns" - ] - for item in delQt5: - qtItem = item.replace("Qt5", "Qt") - unlinkIfFound(binDir / f"{item}.dll") - unlinkIfFound(pyQt5Dir / f"{qtItem}.pyd") - unlinkIfFound(pyQt5Dir / f"{qtItem}.pyi") - deleteFolder(bindDir / qtItem) - - delList = [ - binDir / "opengl32sw.dll", - qt5Dir / "qml", - plugDir / "geoservices", - plugDir / "playlistformats", - plugDir / "renderers", - plugDir / "sensorgestures", - plugDir / "sensors", - plugDir / "sqldrivers", - plugDir / "texttospeech", - plugDir / "webview", - ] - for item in delList: - unlinkIfFound(item) - deleteFolder(item) - - print("Done") - print("") - - print("Running Inno Setup") - print("##################") - print("") - - # Read the iss template - issData = readFile(SETUP_DIR / "win_setup_embed.iss") - issData = issData.replace(r"%%version%%", numVers) - issData = issData.replace(r"%%dist%%", str(bldDir)) - writeFile(CURR_DIR / "setup.iss", issData) - print("") - - try: - subprocess.call(["iscc", "setup.iss"]) - except Exception as exc: - print("Inno Setup failed with error:") - print(str(exc)) - sys.exit(1) - - print("") - print("Done") - print("") - - return - - ## # Generate MacOS PList ## @@ -1181,7 +908,7 @@ def xdgInstall(args: argparse.Namespace) -> None: if testExec is not None: exOpts.append(testExec) - testExec = CURR_DIR / "novelWriter.py" + testExec = ROOT_DIR / "novelWriter.py" if testExec.is_file(): exOpts.append(str(testExec)) @@ -1214,7 +941,7 @@ def xdgInstall(args: argparse.Namespace) -> None: # =========================== # Generate launcher - desktopFile = CURR_DIR / "novelwriter.desktop" + desktopFile = ROOT_DIR / "novelwriter.desktop" desktopData = readFile(SETUP_DIR / "data" / "novelwriter.desktop") desktopData = desktopData.replace("Exec=novelwriter", f"Exec={useExec}") writeFile(desktopFile, desktopData) @@ -1507,7 +1234,7 @@ if __name__ == "__main__": cmdBuildSetupExe = parsers.add_parser( "build-win-exe", help="Build a setup.exe file with Python embedded for Windows." ) - cmdBuildSetupExe.set_defaults(func=makeWindowsEmbedded) + cmdBuildSetupExe.set_defaults(func=utils.windows_build.main) # Build Binary cmdBuildBinary = parsers.add_parser( diff --git a/setup/win_setup_embed.iss b/setup/win_setup_embed.iss index 042914fc..f4c55ac9 100644 --- a/setup/win_setup_embed.iss +++ b/setup/win_setup_embed.iss @@ -39,9 +39,11 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{ Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; Check: not IsAdminInstallMode [InstallDelete] +Type: filesandordirs; Name: "{app}\lib\*" Type: filesandordirs; Name: "{app}\novelwriter\*" [UninstallDelete] +Type: filesandordirs; Name: "{app}\lib\*" Type: filesandordirs; Name: "{app}\novelwriter\*" [Files] diff --git a/utils/common.py b/utils/common.py index 148db07e..2b01781f 100644 --- a/utils/common.py +++ b/utils/common.py @@ -20,6 +20,68 @@ along with this program. If not, see . """ from __future__ import annotations +import shutil + from pathlib import Path ROOT_DIR = Path(__file__).parent.parent +SETUP_DIR = ROOT_DIR / "setup" + + +def extractVersion(beQuiet: bool = False) -> tuple[str, str, str]: + """Extract the novelWriter version number without having to import + anything else from the main package. + """ + def getValue(text: str) -> str: + bits = text.partition("=") + return bits[2].strip().strip('"') + + numVers = "0" + hexVers = "0x0" + relDate = "Unknown" + initFile = Path("novelwriter") / "__init__.py" + try: + for aLine in initFile.read_text(encoding="utf-8").splitlines(): + if aLine.startswith("__version__"): + numVers = getValue((aLine)) + if aLine.startswith("__hexversion__"): + hexVers = getValue((aLine)) + if aLine.startswith("__date__"): + relDate = getValue((aLine)) + except Exception as exc: + print("Could not read file: %s" % initFile) + print(str(exc)) + + if not beQuiet: + print("novelWriter version: %s (%s) at %s" % (numVers, hexVers, relDate)) + + return numVers, hexVers, relDate + + +def copySourceCode(dst: Path) -> None: + """Copy the novelwriter source tree to path.""" + src = ROOT_DIR / "novelwriter" + for item in src.glob("**/*"): + relSrc = item.relative_to(ROOT_DIR) + if item.suffix in (".pyc", ".pyo"): + print(f"Ignore: {relSrc}") + continue + if item.parent.is_dir() and item.parent.name != "__pycache__": + dstDir = dst / relSrc.parent + if not dstDir.exists(): + dstDir.mkdir(parents=True) + print(f"Folder: {dstDir}") + if item.is_file(): + shutil.copyfile(item, dst / relSrc) + print(f"Copied: {dst / relSrc}") + return + + +def readFile(file: Path) -> str: + """Read an entire file and return as a string.""" + return file.read_text(encoding="utf-8") + + +def writeFile(file: Path, text: str) -> int: + """Write string to file.""" + return file.write_text(text, encoding="utf-8") diff --git a/utils/windows_build.py b/utils/windows_build.py new file mode 100644 index 00000000..0b568c50 --- /dev/null +++ b/utils/windows_build.py @@ -0,0 +1,247 @@ +""" +novelWriter – Windows Build +=========================== + +This file is a part of novelWriter +Copyright (C) 2025 Veronica Berglyd Olsen and novelWriter contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +from __future__ import annotations + +import argparse +import compileall +import shutil +import subprocess +import sys +import urllib.request +import zipfile + +from pathlib import Path + +from utils.common import ROOT_DIR, SETUP_DIR, copySourceCode, extractVersion, writeFile + +from tests.tools import readFile + + +def prepareCode(outDir: Path) -> None: + """Set up folders and copy code.""" + print("Copying and compiling novelWriter source ...") + print("") + + copySourceCode(outDir) + + files = [ + ROOT_DIR / "CREDITS.md", + ROOT_DIR / "LICENSE.md", + ROOT_DIR / "requirements.txt", + SETUP_DIR / "icons" / "novelwriter.ico", + SETUP_DIR / "iss_license.txt", + + ] + for item in files: + shutil.copyfile(item, outDir / item.name) + print(f"Copied: {item} > {outDir / item.name}") + + compileall.compile_dir(outDir / "novelwriter") + + print("Done") + print("") + + return + + +def embedPython(bldDir: Path, outDir: Path) -> None: + """Embed Python library.""" + print("Adding Python embeddable ...") + + pyVers = "%d.%d.%d" % (sys.version_info[:3]) + zipFile = f"python-{pyVers}-embed-amd64.zip" + pyZip = bldDir / zipFile + if not pyZip.is_file(): + pyUrl = f"https://www.python.org/ftp/python/{pyVers}/{zipFile}" + print("Downloading: %s" % pyUrl) + urllib.request.urlretrieve(pyUrl, pyZip) + + print("Extracting ...") + with zipfile.ZipFile(pyZip, "r") as inFile: + inFile.extractall(outDir) + + print("Done") + print("") + + return + + +def installRequirements(libDir: Path) -> None: + """Install dependencies.""" + print("Install dependencies ...") + + try: + subprocess.call([ + sys.executable, "-m", + "pip", "install", "-r", "requirements.txt", "--target", str(libDir) + ]) + except Exception as exc: + print("Failed with error:") + print(str(exc)) + sys.exit(1) + + print("Done") + print("") + + return + + +def removeRedundantQt(libDir: Path) -> None: + """Delete Qt files that are not needed""" + + def unlinkIfFound(file: Path) -> None: + if file.is_file(): + file.unlink() + print(f"Deleted: {file}") + + def unlinkIfPrefix(folder: Path, prefix: tuple[str, ...]) -> None: + if folder.is_dir(): + for item in folder.iterdir(): + if item.name.startswith(prefix): + unlinkIfFound(item) + + def deleteFolder(folder: Path) -> None: + if folder.is_dir(): + shutil.rmtree(folder) + print(f"Deleted: {folder}") + + print("Deleting Redundant Files") + print("========================") + print("") + + pyQt6Dir = libDir / "PyQt6" + bindDir = libDir / "PyQt6" / "bindings" + qt6Dir = libDir / "PyQt6" / "Qt6" + binDir = libDir / "PyQt6" / "Qt6" / "bin" + plugDir = libDir / "PyQt6" / "Qt6" / "plugins" + qmDir = libDir / "PyQt6" / "Qt6" / "translations" + dictDir = libDir / "enchant" / "data" / "mingw64" / "share" / "enchant" / "hunspell" + + for item in dictDir.iterdir(): + if not item.name.startswith(("en_GB", "en_US")): + unlinkIfFound(item) + + for item in qmDir.iterdir(): + if not item.name.startswith("qtbase"): + unlinkIfFound(item) + + bulkDel = ("QtQml", "Qt6Qml", "QtQuick", "Qt6Quick") + unlinkIfPrefix(pyQt6Dir, bulkDel) + unlinkIfPrefix(binDir, bulkDel) + + delQt6 = [ + "Qt6Bluetooth", "Qt6DBus", "Qt6Designer", "Qt6Help", "Qt6Multimedia", + "Qt6MultimediaWidgets", "Qt6Network", "Qt6Nfc", "Qt6OpenGL", "Qt6Positioning", + "Qt6PositioningQuick", "Qt6Sensors", "Qt6SerialPort", "Qt6Sql", "Qt6Test", + "Qt6TextToSpeech", "Qt6WebChannel", "Qt6WebSockets", "Qt6Xml", + ] + for item in delQt6: + qtItem = item.replace("Qt6", "Qt") + unlinkIfFound(binDir / f"{item}.dll") + unlinkIfFound(pyQt6Dir / f"{qtItem}.pyd") + unlinkIfFound(pyQt6Dir / f"{qtItem}.pyi") + deleteFolder(bindDir / qtItem) + + delList = [ + binDir / "opengl32sw.dll", + qt6Dir / "qml", + plugDir / "renderers", + plugDir / "sensors", + plugDir / "sqldrivers", + plugDir / "texttospeech", + plugDir / "webview", + ] + for item in delList: + unlinkIfFound(item) + deleteFolder(item) + + print("Done") + print("") + + return + + +def main(args: argparse.Namespace) -> None: + """Set up a package with embedded Python and dependencies for + Windows installation. + """ + print("") + print("Build Standalone Windows Package") + print("================================") + print("") + + numVers, _, _ = extractVersion() + print("Version: %s" % numVers) + + bldDir = ROOT_DIR / "dist" + outDir = bldDir / "novelWriter" + libDir = outDir / "lib" + if outDir.exists(): + shutil.rmtree(outDir) + + bldDir.mkdir(exist_ok=True) + outDir.mkdir() + libDir.mkdir() + + copySourceCode(outDir) + embedPython(bldDir, outDir) + installRequirements(libDir) + removeRedundantQt(libDir) + + print("Updating starting script ...") + writeFile(outDir / "novelWriter.pyw", ( + "#!/usr/bin/env python3\n" + "import os\n" + "import sys\n" + "\n" + "os.curdir = os.path.abspath(os.path.dirname(__file__))\n" + "sys.path.insert(0, os.path.join(os.curdir, \"lib\"))\n" + "\n" + "if __name__ == \"__main__\":\n" + " import novelwriter\n" + " novelwriter.main(sys.argv[1:])\n" + )) + print("Done") + print("") + + print("Running Inno Setup") + print("##################") + print("") + + # Read the iss template + issData = readFile(SETUP_DIR / "win_setup_embed.iss") + issData = issData.replace(r"%%version%%", numVers) + issData = issData.replace(r"%%dist%%", str(bldDir)) + writeFile(ROOT_DIR / "setup.iss", issData) + print("") + + try: + subprocess.call(["iscc", "setup.iss"]) + except Exception as exc: + print("Inno Setup failed with error:") + print(str(exc)) + sys.exit(1) + + print("") + print("Done") + print("") + + return