Move and update Windows build for Qt6
This commit is contained in:
+35
-308
@@ -37,9 +37,10 @@ from pathlib import Path
|
|||||||
|
|
||||||
import utils.binary_dist
|
import utils.binary_dist
|
||||||
import utils.icon_themes
|
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"
|
SIGN_KEY = "D6A9F6B8F227CF7C6F6D1EE84DBBE4B734B0BD08"
|
||||||
|
|
||||||
OS_LINUX = sys.platform.startswith("linux")
|
OS_LINUX = sys.platform.startswith("linux")
|
||||||
@@ -51,36 +52,6 @@ OS_WIN = sys.platform.startswith("win32")
|
|||||||
# Utilities
|
# 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:
|
def stripVersion(version: str) -> str:
|
||||||
"""Strip the pre-release part from a version number."""
|
"""Strip the pre-release part from a version number."""
|
||||||
if "a" in version:
|
if "a" in version:
|
||||||
@@ -93,16 +64,6 @@ def stripVersion(version: str) -> str:
|
|||||||
return version
|
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:
|
def toUpload(srcPath: str | Path, dstName: str | None = None) -> None:
|
||||||
"""Copy a file produced by one of the build functions to the upload
|
"""Copy a file produced by one of the build functions to the upload
|
||||||
directory. The file can optionally be given a new name.
|
directory. The file can optionally be given a new name.
|
||||||
@@ -138,17 +99,17 @@ def checkAssetsExist() -> bool:
|
|||||||
hasManual = False
|
hasManual = False
|
||||||
hasQmData = False
|
hasQmData = False
|
||||||
|
|
||||||
sampleZip = CURR_DIR / "novelwriter" / "assets" / "sample.zip"
|
sampleZip = ROOT_DIR / "novelwriter" / "assets" / "sample.zip"
|
||||||
if sampleZip.is_file():
|
if sampleZip.is_file():
|
||||||
print(f"Found: {sampleZip}")
|
print(f"Found: {sampleZip}")
|
||||||
hasSample = True
|
hasSample = True
|
||||||
|
|
||||||
pdfManual = CURR_DIR / "novelwriter" / "assets" / "manual.pdf"
|
pdfManual = ROOT_DIR / "novelwriter" / "assets" / "manual.pdf"
|
||||||
if pdfManual.is_file():
|
if pdfManual.is_file():
|
||||||
print(f"Found: {pdfManual}")
|
print(f"Found: {pdfManual}")
|
||||||
hasManual = True
|
hasManual = True
|
||||||
|
|
||||||
i18nAssets = CURR_DIR / "novelwriter" / "assets" / "i18n"
|
i18nAssets = ROOT_DIR / "novelwriter" / "assets" / "i18n"
|
||||||
if len(list(i18nAssets.glob("*.qm"))) > 0:
|
if len(list(i18nAssets.glob("*.qm"))) > 0:
|
||||||
print(f"Found: {i18nAssets}/*.qm")
|
print(f"Found: {i18nAssets}/*.qm")
|
||||||
hasQmData = True
|
hasQmData = True
|
||||||
@@ -214,12 +175,12 @@ def cleanBuildDirs(args: argparse.Namespace) -> None:
|
|||||||
print("")
|
print("")
|
||||||
|
|
||||||
folders = [
|
folders = [
|
||||||
CURR_DIR / "build",
|
ROOT_DIR / "build",
|
||||||
CURR_DIR / "dist",
|
ROOT_DIR / "dist",
|
||||||
CURR_DIR / "dist_deb",
|
ROOT_DIR / "dist_deb",
|
||||||
CURR_DIR / "dist_minimal",
|
ROOT_DIR / "dist_minimal",
|
||||||
CURR_DIR / "dist_appimage",
|
ROOT_DIR / "dist_appimage",
|
||||||
CURR_DIR / "novelWriter.egg-info",
|
ROOT_DIR / "novelWriter.egg-info",
|
||||||
]
|
]
|
||||||
|
|
||||||
for folder in folders:
|
for folder in folders:
|
||||||
@@ -252,8 +213,8 @@ def buildPdfManual(args: argparse.Namespace | None = None) -> None:
|
|||||||
print("===================")
|
print("===================")
|
||||||
print("")
|
print("")
|
||||||
|
|
||||||
buildFile = CURR_DIR / "docs" / "build" / "latex" / "manual.pdf"
|
buildFile = ROOT_DIR / "docs" / "build" / "latex" / "manual.pdf"
|
||||||
finalFile = CURR_DIR / "novelwriter" / "assets" / "manual.pdf"
|
finalFile = ROOT_DIR / "novelwriter" / "assets" / "manual.pdf"
|
||||||
finalFile.unlink(missing_ok=True)
|
finalFile.unlink(missing_ok=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -303,8 +264,8 @@ def buildSampleZip(args: argparse.Namespace | None = None) -> None:
|
|||||||
print("========================")
|
print("========================")
|
||||||
print("")
|
print("")
|
||||||
|
|
||||||
srcSample = CURR_DIR / "sample"
|
srcSample = ROOT_DIR / "sample"
|
||||||
dstSample = CURR_DIR / "novelwriter" / "assets" / "sample.zip"
|
dstSample = ROOT_DIR / "novelwriter" / "assets" / "sample.zip"
|
||||||
|
|
||||||
if srcSample.is_dir():
|
if srcSample.is_dir():
|
||||||
dstSample.unlink(missing_ok=True)
|
dstSample.unlink(missing_ok=True)
|
||||||
@@ -342,8 +303,8 @@ def importI18nUpdates(args: argparse.Namespace) -> None:
|
|||||||
print("File not found ...")
|
print("File not found ...")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
dstPath = CURR_DIR / "novelwriter" / "assets" / "i18n"
|
dstPath = ROOT_DIR / "novelwriter" / "assets" / "i18n"
|
||||||
srcPath = CURR_DIR / "i18n"
|
srcPath = ROOT_DIR / "i18n"
|
||||||
|
|
||||||
print(f"Loading file: {fileName}")
|
print(f"Loading file: {fileName}")
|
||||||
with zipfile.ZipFile(fileName) as zipObj:
|
with zipfile.ZipFile(fileName) as zipObj:
|
||||||
@@ -384,10 +345,10 @@ def updateTranslationSources(args: argparse.Namespace) -> None:
|
|||||||
print("Scanning Source Tree:")
|
print("Scanning Source Tree:")
|
||||||
print("")
|
print("")
|
||||||
|
|
||||||
sources = list((CURR_DIR / "novelwriter").glob("**/*.py"))
|
sources = list((ROOT_DIR / "novelwriter").glob("**/*.py"))
|
||||||
sources.insert(0, CURR_DIR / "i18n" / "qtbase.py")
|
sources.insert(0, ROOT_DIR / "i18n" / "qtbase.py")
|
||||||
for source in sources:
|
for source in sources:
|
||||||
print(source.relative_to(CURR_DIR))
|
print(source.relative_to(ROOT_DIR))
|
||||||
|
|
||||||
print("")
|
print("")
|
||||||
print("TS Files to Update:")
|
print("TS Files to Update:")
|
||||||
@@ -444,8 +405,8 @@ def buildTranslationAssets(args: argparse.Namespace | None = None) -> None:
|
|||||||
print("TS Files to Build:")
|
print("TS Files to Build:")
|
||||||
print("")
|
print("")
|
||||||
|
|
||||||
srcDir = CURR_DIR / "i18n"
|
srcDir = ROOT_DIR / "i18n"
|
||||||
dstDir = CURR_DIR / "novelwriter" / "assets" / "i18n"
|
dstDir = ROOT_DIR / "novelwriter" / "assets" / "i18n"
|
||||||
|
|
||||||
srcList = []
|
srcList = []
|
||||||
for item in srcDir.iterdir():
|
for item in srcDir.iterdir():
|
||||||
@@ -469,11 +430,11 @@ def buildTranslationAssets(args: argparse.Namespace | None = None) -> None:
|
|||||||
print("Moving QM Files to Assets")
|
print("Moving QM Files to Assets")
|
||||||
print("")
|
print("")
|
||||||
|
|
||||||
dstRel = dstDir.relative_to(CURR_DIR)
|
dstRel = dstDir.relative_to(ROOT_DIR)
|
||||||
for item in srcDir.iterdir():
|
for item in srcDir.iterdir():
|
||||||
if item.is_file() and item.suffix == ".qm":
|
if item.is_file() and item.suffix == ".qm":
|
||||||
item.rename(dstDir / item.name)
|
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("")
|
print("")
|
||||||
|
|
||||||
@@ -492,14 +453,14 @@ def cleanBuiltAssets(args: argparse.Namespace | None = None) -> None:
|
|||||||
print("")
|
print("")
|
||||||
|
|
||||||
assets = [
|
assets = [
|
||||||
CURR_DIR / "novelwriter" / "assets" / "sample.zip",
|
ROOT_DIR / "novelwriter" / "assets" / "sample.zip",
|
||||||
CURR_DIR / "novelwriter" / "assets" / "manual.pdf",
|
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:
|
for asset in assets:
|
||||||
if asset.is_file():
|
if asset.is_file():
|
||||||
asset.unlink()
|
asset.unlink()
|
||||||
print(f"Deleted: {asset.relative_to(CURR_DIR)}")
|
print(f"Deleted: {asset.relative_to(ROOT_DIR)}")
|
||||||
|
|
||||||
print("")
|
print("")
|
||||||
|
|
||||||
@@ -523,29 +484,6 @@ def buildAllAssets(args: argparse.Namespace) -> None:
|
|||||||
# Python Packaging
|
# 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
|
# Copy Package Files
|
||||||
##
|
##
|
||||||
@@ -572,7 +510,7 @@ def copyPackageFiles(dst: Path, setupPy: bool = False) -> None:
|
|||||||
))
|
))
|
||||||
print("Wrote: setup.py")
|
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")
|
text = text.replace("setup/description_pypi.md", "data/description_short.txt")
|
||||||
writeFile(dst / "pyproject.toml", text)
|
writeFile(dst / "pyproject.toml", text)
|
||||||
print("Wrote: pyproject.toml")
|
print("Wrote: pyproject.toml")
|
||||||
@@ -613,7 +551,7 @@ def makeDebianPackage(
|
|||||||
# Set Up Folder
|
# Set Up Folder
|
||||||
# =============
|
# =============
|
||||||
|
|
||||||
bldDir = CURR_DIR / "dist_deb"
|
bldDir = ROOT_DIR / "dist_deb"
|
||||||
bldPkg = f"novelwriter_{pkgVers}"
|
bldPkg = f"novelwriter_{pkgVers}"
|
||||||
outDir = bldDir / bldPkg
|
outDir = bldDir / bldPkg
|
||||||
debDir = outDir / "debian"
|
debDir = outDir / "debian"
|
||||||
@@ -814,7 +752,7 @@ def buildAppImage(args: argparse.Namespace) -> None:
|
|||||||
# Set Up Folder
|
# Set Up Folder
|
||||||
# =============
|
# =============
|
||||||
|
|
||||||
bldDir = CURR_DIR / "dist_appimage"
|
bldDir = ROOT_DIR / "dist_appimage"
|
||||||
bldPkg = f"novelwriter_{pkgVers}"
|
bldPkg = f"novelwriter_{pkgVers}"
|
||||||
outDir = bldDir / bldPkg
|
outDir = bldDir / bldPkg
|
||||||
imgDir = bldDir / "appimage"
|
imgDir = bldDir / "appimage"
|
||||||
@@ -915,217 +853,6 @@ def buildAppImage(args: argparse.Namespace) -> None:
|
|||||||
return
|
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
|
# Generate MacOS PList
|
||||||
##
|
##
|
||||||
@@ -1181,7 +908,7 @@ def xdgInstall(args: argparse.Namespace) -> None:
|
|||||||
if testExec is not None:
|
if testExec is not None:
|
||||||
exOpts.append(testExec)
|
exOpts.append(testExec)
|
||||||
|
|
||||||
testExec = CURR_DIR / "novelWriter.py"
|
testExec = ROOT_DIR / "novelWriter.py"
|
||||||
if testExec.is_file():
|
if testExec.is_file():
|
||||||
exOpts.append(str(testExec))
|
exOpts.append(str(testExec))
|
||||||
|
|
||||||
@@ -1214,7 +941,7 @@ def xdgInstall(args: argparse.Namespace) -> None:
|
|||||||
# ===========================
|
# ===========================
|
||||||
|
|
||||||
# Generate launcher
|
# Generate launcher
|
||||||
desktopFile = CURR_DIR / "novelwriter.desktop"
|
desktopFile = ROOT_DIR / "novelwriter.desktop"
|
||||||
desktopData = readFile(SETUP_DIR / "data" / "novelwriter.desktop")
|
desktopData = readFile(SETUP_DIR / "data" / "novelwriter.desktop")
|
||||||
desktopData = desktopData.replace("Exec=novelwriter", f"Exec={useExec}")
|
desktopData = desktopData.replace("Exec=novelwriter", f"Exec={useExec}")
|
||||||
writeFile(desktopFile, desktopData)
|
writeFile(desktopFile, desktopData)
|
||||||
@@ -1507,7 +1234,7 @@ if __name__ == "__main__":
|
|||||||
cmdBuildSetupExe = parsers.add_parser(
|
cmdBuildSetupExe = parsers.add_parser(
|
||||||
"build-win-exe", help="Build a setup.exe file with Python embedded for Windows."
|
"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
|
# Build Binary
|
||||||
cmdBuildBinary = parsers.add_parser(
|
cmdBuildBinary = parsers.add_parser(
|
||||||
|
|||||||
@@ -39,9 +39,11 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{
|
|||||||
Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; Check: not IsAdminInstallMode
|
Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; Check: not IsAdminInstallMode
|
||||||
|
|
||||||
[InstallDelete]
|
[InstallDelete]
|
||||||
|
Type: filesandordirs; Name: "{app}\lib\*"
|
||||||
Type: filesandordirs; Name: "{app}\novelwriter\*"
|
Type: filesandordirs; Name: "{app}\novelwriter\*"
|
||||||
|
|
||||||
[UninstallDelete]
|
[UninstallDelete]
|
||||||
|
Type: filesandordirs; Name: "{app}\lib\*"
|
||||||
Type: filesandordirs; Name: "{app}\novelwriter\*"
|
Type: filesandordirs; Name: "{app}\novelwriter\*"
|
||||||
|
|
||||||
[Files]
|
[Files]
|
||||||
|
|||||||
@@ -20,6 +20,68 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
ROOT_DIR = Path(__file__).parent.parent
|
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")
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user