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.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(
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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