Move and update Windows build for Qt6

This commit is contained in:
Veronica Berglyd Olsen
2025-01-16 21:03:00 +01:00
parent 931863d4ee
commit 767c22f348
4 changed files with 346 additions and 308 deletions
+35 -308
View File
@@ -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(
+2
View File
@@ -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]
+62
View File
@@ -20,6 +20,68 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
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")
+247
View File
@@ -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 <https://www.gnu.org/licenses/>.
"""
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