Clean up pkgutils and update Windows installer (#2196)
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
# Setup/Install
|
# Setup/Install
|
||||||
/MANIFEST
|
/MANIFEST
|
||||||
/build/
|
/build/
|
||||||
|
/build_*/
|
||||||
/deploy/
|
/deploy/
|
||||||
/dist/
|
/dist/
|
||||||
/dist_*/
|
/dist_*/
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# Meta
|
# Meta
|
||||||
meta:name = Font Awesome 6
|
meta:name = Font Awesome 6
|
||||||
meta:author = Fonticons Inc
|
meta:author = Fonticons Inc
|
||||||
meta:license = Font Awesome Free License
|
meta:license = CC BY 4.0
|
||||||
|
|
||||||
# Icons
|
# Icons
|
||||||
icon:alert_error = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="#000000" height="128" width="128"><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zm0-384c13.3 0 24 10.7 24 24l0 112c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-112c0-13.3 10.7-24 24-24zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z" /></svg>
|
icon:alert_error = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="#000000" height="128" width="128"><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zm0-384c13.3 0 24 10.7 24 24l0 112c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-112c0-13.3 10.7-24 24-24zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z" /></svg>
|
||||||
|
|||||||
@@ -109,10 +109,10 @@ class Config:
|
|||||||
|
|
||||||
self._appPath = Path(__file__).parent.absolute()
|
self._appPath = Path(__file__).parent.absolute()
|
||||||
self._appRoot = self._appPath.parent
|
self._appRoot = self._appPath.parent
|
||||||
if self._appRoot.is_file():
|
if getattr(sys, "frozen", False): # pragma: no cover
|
||||||
# novelWriter is packaged as a single file
|
# novelWriter is packaged as an exe
|
||||||
self._appRoot = self._appRoot.parent
|
self._appPath = Path(__file__).parent.parent.absolute()
|
||||||
self._appPath = self._appRoot
|
self._appRoot = self._appPath
|
||||||
|
|
||||||
# Runtime Settings and Variables
|
# Runtime Settings and Variables
|
||||||
self._hasError = False # True if the config class encountered an error
|
self._hasError = False # True if the config class encountered an error
|
||||||
|
|||||||
+36
-1365
File diff suppressed because it is too large
Load Diff
@@ -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]
|
||||||
|
|||||||
@@ -84,12 +84,6 @@ def testBaseConfig_Constructor(monkeypatch):
|
|||||||
assert tstConf.osWindows is False
|
assert tstConf.osWindows is False
|
||||||
assert tstConf.osUnknown is True
|
assert tstConf.osUnknown is True
|
||||||
|
|
||||||
# App is single file
|
|
||||||
with monkeypatch.context() as mp:
|
|
||||||
mp.setattr("pathlib.Path.is_file", lambda *a: True)
|
|
||||||
tstConf = Config()
|
|
||||||
assert tstConf._appPath == tstConf._appRoot
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.base
|
@pytest.mark.base
|
||||||
def testBaseConfig_InitLoadSave(monkeypatch, fncPath, tstPaths):
|
def testBaseConfig_InitLoadSave(monkeypatch, fncPath, tstPaths):
|
||||||
|
|||||||
+279
@@ -0,0 +1,279 @@
|
|||||||
|
"""
|
||||||
|
novelWriter – Assets
|
||||||
|
====================
|
||||||
|
|
||||||
|
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 subprocess
|
||||||
|
import sys
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from utils.common import ROOT_DIR, writeFile
|
||||||
|
|
||||||
|
|
||||||
|
def buildPdfManual(args: argparse.Namespace | None = None) -> None:
|
||||||
|
"""This function will build the documentation as manual.pdf."""
|
||||||
|
print("")
|
||||||
|
print("Building PDF Manual")
|
||||||
|
print("===================")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
buildFile = ROOT_DIR / "docs" / "build" / "latex" / "manual.pdf"
|
||||||
|
finalFile = ROOT_DIR / "novelwriter" / "assets" / "manual.pdf"
|
||||||
|
finalFile.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.call(["make", "clean"], cwd="docs")
|
||||||
|
exCode = subprocess.call(["make", "latexpdf"], cwd="docs")
|
||||||
|
if exCode == 0:
|
||||||
|
print("")
|
||||||
|
buildFile.rename(finalFile)
|
||||||
|
else:
|
||||||
|
raise Exception(f"Build returned error code {exCode}")
|
||||||
|
|
||||||
|
print("PDF manual build: OK")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
print("PDF manual build: FAILED")
|
||||||
|
print("")
|
||||||
|
print(str(exc))
|
||||||
|
print("")
|
||||||
|
print("Dependencies:")
|
||||||
|
print(" * pip install sphinx")
|
||||||
|
print(" * Package latexmk")
|
||||||
|
print(" * LaTeX build system")
|
||||||
|
print("")
|
||||||
|
print(" On Debian/Ubuntu, install: python3-sphinx latexmk texlive texlive-latex-extra")
|
||||||
|
print("")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not finalFile.is_file():
|
||||||
|
print("No output file was found!")
|
||||||
|
print("")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def buildSampleZip(args: argparse.Namespace | None = None) -> None:
|
||||||
|
"""Bundle the sample project into a single zip file to be saved into
|
||||||
|
the novelwriter/assets folder for further bundling into builds.
|
||||||
|
"""
|
||||||
|
print("")
|
||||||
|
print("Building Sample ZIP File")
|
||||||
|
print("========================")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
srcSample = ROOT_DIR / "sample"
|
||||||
|
dstSample = ROOT_DIR / "novelwriter" / "assets" / "sample.zip"
|
||||||
|
|
||||||
|
if srcSample.is_dir():
|
||||||
|
dstSample.unlink(missing_ok=True)
|
||||||
|
with zipfile.ZipFile(dstSample, "w") as zipObj:
|
||||||
|
print("Compressing: nwProject.nwx")
|
||||||
|
zipObj.write(srcSample / "nwProject.nwx", "nwProject.nwx")
|
||||||
|
for doc in (srcSample / "content").iterdir():
|
||||||
|
print(f"Compressing: content/{doc.name}")
|
||||||
|
zipObj.write(doc, f"content/{doc.name}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("Error: Could not find sample project source directory.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("Built file: %s" % dstSample)
|
||||||
|
print("")
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def importI18nUpdates(args: argparse.Namespace) -> None:
|
||||||
|
"""Import new translation files from a zip file."""
|
||||||
|
print("")
|
||||||
|
print("Import Updated Translations")
|
||||||
|
print("===========================")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
fileName = Path(args.file).absolute()
|
||||||
|
if not fileName.is_file():
|
||||||
|
print("File not found ...")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
dstPath = ROOT_DIR / "novelwriter" / "assets" / "i18n"
|
||||||
|
srcPath = ROOT_DIR / "i18n"
|
||||||
|
|
||||||
|
print(f"Loading file: {fileName}")
|
||||||
|
with zipfile.ZipFile(fileName) as zipObj:
|
||||||
|
for item in zipObj.namelist():
|
||||||
|
if item.startswith("nw_") and item.endswith(".ts"):
|
||||||
|
zipObj.extract(item, srcPath)
|
||||||
|
print(f"Extracted: {item} > {srcPath / item}")
|
||||||
|
elif item.startswith("project_") and item.endswith(".json"):
|
||||||
|
zipObj.extract(item, dstPath)
|
||||||
|
print(f"Extracted: {item} > {dstPath / item}")
|
||||||
|
else:
|
||||||
|
print(f"Skipped: {item}")
|
||||||
|
|
||||||
|
print("")
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def updateTranslationSources(args: argparse.Namespace) -> None:
|
||||||
|
"""Build the lang.ts files for Qt Linguist."""
|
||||||
|
print("")
|
||||||
|
print("Building Qt Translation Files")
|
||||||
|
print("=============================")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PyQt6.lupdate.lupdate import lupdate
|
||||||
|
except ImportError:
|
||||||
|
print("ERROR: This command requires lupdate from PyQt6")
|
||||||
|
print("On Debian/Ubuntu, install: pyqt6-dev-tools")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("Scanning Source Tree:")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
sources = list((ROOT_DIR / "novelwriter").glob("**/*.py"))
|
||||||
|
sources.insert(0, ROOT_DIR / "i18n" / "qtbase.py")
|
||||||
|
for source in sources:
|
||||||
|
print(source.relative_to(ROOT_DIR))
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("TS Files to Update:")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
translations = []
|
||||||
|
for item in [Path(str(f)).absolute() for f in args.files]:
|
||||||
|
if not (item.name.startswith("nw_") and item.suffix == ".ts"):
|
||||||
|
print(f"Skipped: {item}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if item.is_file():
|
||||||
|
translations.append(item)
|
||||||
|
print(f"Added: {item}")
|
||||||
|
elif item.exists():
|
||||||
|
continue
|
||||||
|
else: # Create an empty new language file
|
||||||
|
langCode = item.name[3:-3]
|
||||||
|
writeFile(item, (
|
||||||
|
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
|
||||||
|
"<!DOCTYPE TS>\n"
|
||||||
|
f"<TS version=\"2.0\" language=\"{langCode}\" sourcelanguage=\"en_GB\"/>\n"
|
||||||
|
))
|
||||||
|
translations.append(item)
|
||||||
|
print(f"Created: {item}")
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("Updating Language Files:")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
lupdate(
|
||||||
|
sources=[str(f) for f in sources],
|
||||||
|
translation_files=[str(f) for f in translations],
|
||||||
|
no_obsolete=True,
|
||||||
|
no_summary=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
print("")
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def buildTranslationAssets(args: argparse.Namespace | None = None) -> None:
|
||||||
|
"""Build the lang.qm files for Qt Linguist."""
|
||||||
|
print("")
|
||||||
|
print("Building Qt Localisation Files")
|
||||||
|
print("==============================")
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("TS Files to Build:")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
srcDir = ROOT_DIR / "i18n"
|
||||||
|
dstDir = ROOT_DIR / "novelwriter" / "assets" / "i18n"
|
||||||
|
|
||||||
|
srcList = []
|
||||||
|
for item in srcDir.iterdir():
|
||||||
|
if item.is_file() and item.suffix == ".ts" and item.name != "nw_base.ts":
|
||||||
|
srcList.append(item)
|
||||||
|
print(item)
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("Building Translation Files:")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.call(["lrelease", "-verbose", *srcList])
|
||||||
|
except Exception as exc:
|
||||||
|
print("Qt Linguist tools seem to be missing")
|
||||||
|
print("On Debian/Ubuntu, install: qttools5-dev-tools")
|
||||||
|
print(str(exc))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("Moving QM Files to Assets")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
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(ROOT_DIR), dstRel / item.name))
|
||||||
|
|
||||||
|
print("")
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def cleanBuiltAssets(args: argparse.Namespace | None = None) -> None:
|
||||||
|
"""Remove assets built by this script."""
|
||||||
|
print("")
|
||||||
|
print("Removing Built Assets")
|
||||||
|
print("=====================")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
assets = [
|
||||||
|
ROOT_DIR / "novelwriter" / "assets" / "sample.zip",
|
||||||
|
ROOT_DIR / "novelwriter" / "assets" / "manual.pdf",
|
||||||
|
]
|
||||||
|
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(ROOT_DIR)}")
|
||||||
|
|
||||||
|
print("")
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def buildAllAssets(args: argparse.Namespace) -> None:
|
||||||
|
"""Build all assets."""
|
||||||
|
cleanBuiltAssets()
|
||||||
|
buildPdfManual()
|
||||||
|
buildSampleZip()
|
||||||
|
buildTranslationAssets()
|
||||||
|
return
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
"""
|
||||||
|
novelWriter – AppImage 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 datetime
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from utils.common import (
|
||||||
|
ROOT_DIR, SETUP_DIR, copyPackageFiles, copySourceCode, extractVersion,
|
||||||
|
makeCheckSum, readFile, toUpload, writeFile
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def appImage(args: argparse.Namespace) -> None:
|
||||||
|
"""Build an AppImage."""
|
||||||
|
try:
|
||||||
|
import python_appimage # noqa: F401 # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
print(
|
||||||
|
"ERROR: Package 'python-appimage' is missing on this system.\n"
|
||||||
|
" Please run 'pip install --user python-appimage' to install it.\n"
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if sys.platform == "linux":
|
||||||
|
print("ERROR: Command 'build-ubuntu' can only be used on Linux")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("Build AppImage")
|
||||||
|
print("==============")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
linuxTag = args.linux_tag
|
||||||
|
pythonVer = args.python_version
|
||||||
|
|
||||||
|
# Version Info
|
||||||
|
# ============
|
||||||
|
|
||||||
|
pkgVers, _, relDate = extractVersion()
|
||||||
|
relDate = datetime.datetime.strptime(relDate, "%Y-%m-%d")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
# Set Up Folder
|
||||||
|
# =============
|
||||||
|
|
||||||
|
bldDir = ROOT_DIR / "dist_appimage"
|
||||||
|
bldPkg = f"novelwriter_{pkgVers}"
|
||||||
|
outDir = bldDir / bldPkg
|
||||||
|
imgDir = bldDir / "appimage"
|
||||||
|
|
||||||
|
# Set Up Folders
|
||||||
|
# ==============
|
||||||
|
|
||||||
|
bldDir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
if outDir.exists():
|
||||||
|
print("Removing old build files ...")
|
||||||
|
print("")
|
||||||
|
shutil.rmtree(outDir)
|
||||||
|
|
||||||
|
outDir.mkdir()
|
||||||
|
|
||||||
|
if imgDir.exists():
|
||||||
|
print("Removing old build metadata files ...")
|
||||||
|
print("")
|
||||||
|
shutil.rmtree(imgDir)
|
||||||
|
|
||||||
|
imgDir.mkdir()
|
||||||
|
|
||||||
|
# Remove old AppImages
|
||||||
|
if images := bldDir.glob("*.AppImage"):
|
||||||
|
print("Removing old AppImages")
|
||||||
|
print("")
|
||||||
|
for image in images:
|
||||||
|
image.unlink()
|
||||||
|
|
||||||
|
# Copy novelWriter Source
|
||||||
|
# =======================
|
||||||
|
|
||||||
|
print("Copying novelWriter source ...")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
copySourceCode(outDir)
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("Copying or generating additional files ...")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
copyPackageFiles(outDir)
|
||||||
|
|
||||||
|
# Write Metadata
|
||||||
|
# ==============
|
||||||
|
|
||||||
|
appDescription = readFile(SETUP_DIR / "description_short.txt")
|
||||||
|
appdataXML = readFile(SETUP_DIR / "novelwriter.appdata.xml")
|
||||||
|
appdataXML = appdataXML.format(description=appDescription)
|
||||||
|
writeFile(imgDir / "novelwriter.appdata.xml", appdataXML)
|
||||||
|
print("Wrote: novelwriter.appdata.xml")
|
||||||
|
|
||||||
|
writeFile(imgDir / "entrypoint.sh", (
|
||||||
|
'#! /bin/bash \n'
|
||||||
|
'{{ python-executable }} -sE ${APPDIR}/opt/python{{ python-version }}/bin/novelwriter "$@"'
|
||||||
|
))
|
||||||
|
print("Wrote: entrypoint.sh")
|
||||||
|
|
||||||
|
writeFile(imgDir / "requirements.txt", str(outDir))
|
||||||
|
print("Wrote: requirements.txt")
|
||||||
|
|
||||||
|
shutil.copyfile(SETUP_DIR / "data" / "novelwriter.desktop", imgDir / "novelwriter.desktop")
|
||||||
|
print("Copied: novelwriter.desktop")
|
||||||
|
|
||||||
|
shutil.copyfile(SETUP_DIR / "icons" / "novelwriter.svg", imgDir / "novelwriter.svg")
|
||||||
|
print("Copied: novelwriter.svg")
|
||||||
|
|
||||||
|
shutil.copyfile(
|
||||||
|
SETUP_DIR / "data" / "hicolor" / "256x256" / "apps" / "novelwriter.png",
|
||||||
|
imgDir / "novelwriter.png"
|
||||||
|
)
|
||||||
|
print("Copied: novelwriter.png")
|
||||||
|
|
||||||
|
# Build AppImage
|
||||||
|
# ==============
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.call([
|
||||||
|
sys.executable, "-m", "python_appimage", "build", "app",
|
||||||
|
"-l", linuxTag, "-p", pythonVer, "appimage"
|
||||||
|
], cwd=bldDir)
|
||||||
|
except Exception as exc:
|
||||||
|
print("AppImage build: FAILED")
|
||||||
|
print("")
|
||||||
|
print(str(exc))
|
||||||
|
print("")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
bldFile = list(bldDir.glob("*.AppImage"))[0]
|
||||||
|
outFile = bldDir / f"novelWriter-{pkgVers}.AppImage"
|
||||||
|
bldFile.rename(outFile)
|
||||||
|
shaFile = makeCheckSum(outFile.name, cwd=bldDir)
|
||||||
|
|
||||||
|
toUpload(outFile)
|
||||||
|
toUpload(shaFile)
|
||||||
|
|
||||||
|
return
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"""
|
||||||
|
novelWriter – Binary Dist Tools
|
||||||
|
===============================
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def runPyinstaller() -> None:
|
||||||
|
"""Run the pyinstaller."""
|
||||||
|
import PyInstaller.__main__
|
||||||
|
|
||||||
|
build = ["novelWriter.py", "--clean", "--windowed", "--onedir", "--noconfirm"]
|
||||||
|
build += ["--name", "novelwriter"]
|
||||||
|
build += ["--workpath", "build_bin"]
|
||||||
|
build += ["--distpath", "dist_bin"]
|
||||||
|
build += ["--hidden-import", "pyenchant"]
|
||||||
|
build += ["--add-data", "novelwriter/assets:assets"]
|
||||||
|
PyInstaller.__main__.run(build)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def main(args: argparse.Namespace) -> None:
|
||||||
|
"""Entry point function."""
|
||||||
|
runPyinstaller()
|
||||||
|
return
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
"""
|
||||||
|
novelWriter – Debian 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 datetime
|
||||||
|
import email.utils
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from utils.common import (
|
||||||
|
ROOT_DIR, SETUP_DIR, checkAssetsExist, copyPackageFiles, copySourceCode,
|
||||||
|
extractVersion, makeCheckSum, toUpload, writeFile
|
||||||
|
)
|
||||||
|
|
||||||
|
SIGN_KEY = "D6A9F6B8F227CF7C6F6D1EE84DBBE4B734B0BD08"
|
||||||
|
|
||||||
|
|
||||||
|
def makeDebianPackage(
|
||||||
|
signKey: str | None = None, sourceBuild: bool = False, distName: str = "unstable",
|
||||||
|
buildName: str = "", forLaunchpad: bool = False
|
||||||
|
) -> str:
|
||||||
|
"""Build a Debian package."""
|
||||||
|
print("")
|
||||||
|
print("Build Debian Package")
|
||||||
|
print("====================")
|
||||||
|
print("On Debian/Ubuntu install: dh-python python3-all debhelper devscripts ")
|
||||||
|
print(" pybuild-plugin-pyproject")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
# Version Info
|
||||||
|
# ============
|
||||||
|
|
||||||
|
numVers, hexVers, relDate = extractVersion()
|
||||||
|
relDate = datetime.datetime.strptime(relDate, "%Y-%m-%d")
|
||||||
|
pkgDate = email.utils.format_datetime(relDate.replace(hour=12, tzinfo=None))
|
||||||
|
print("")
|
||||||
|
|
||||||
|
if forLaunchpad:
|
||||||
|
pkgVers = numVers.replace("a", "~a").replace("b", "~b").replace("rc", "~rc")
|
||||||
|
else:
|
||||||
|
pkgVers = numVers
|
||||||
|
pkgVers = f"{pkgVers}+{buildName}" if buildName else pkgVers
|
||||||
|
|
||||||
|
# Set Up Folder
|
||||||
|
# =============
|
||||||
|
|
||||||
|
bldDir = ROOT_DIR / "dist_deb"
|
||||||
|
bldPkg = f"novelwriter_{pkgVers}"
|
||||||
|
outDir = bldDir / bldPkg
|
||||||
|
debDir = outDir / "debian"
|
||||||
|
datDir = outDir / "data"
|
||||||
|
|
||||||
|
bldDir.mkdir(exist_ok=True)
|
||||||
|
if outDir.exists():
|
||||||
|
print("Removing old build files ...")
|
||||||
|
print("")
|
||||||
|
shutil.rmtree(outDir)
|
||||||
|
|
||||||
|
outDir.mkdir(exist_ok=False)
|
||||||
|
|
||||||
|
# Check Additional Assets
|
||||||
|
# =======================
|
||||||
|
|
||||||
|
if not checkAssetsExist():
|
||||||
|
print("ERROR: Missing build assets")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Copy novelWriter Source
|
||||||
|
# =======================
|
||||||
|
|
||||||
|
print("Copying novelWriter source ...")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
copySourceCode(outDir)
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("Copying or generating additional files ...")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
copyPackageFiles(outDir, setupPy=True)
|
||||||
|
|
||||||
|
# Copy/Write Debian Files
|
||||||
|
# =======================
|
||||||
|
|
||||||
|
shutil.copytree(SETUP_DIR / "debian", debDir)
|
||||||
|
print("Copied: debian/*")
|
||||||
|
|
||||||
|
writeFile(debDir / "changelog", (
|
||||||
|
f"novelwriter ({pkgVers}) {distName}; urgency=low\n\n"
|
||||||
|
f" * Update to version {pkgVers}\n\n"
|
||||||
|
f" -- Veronica Berglyd Olsen <code@vkbo.net> {pkgDate}\n"
|
||||||
|
))
|
||||||
|
print("Wrote: debian/changelog")
|
||||||
|
|
||||||
|
# Copy/Write Data Files
|
||||||
|
# =====================
|
||||||
|
|
||||||
|
shutil.copytree(SETUP_DIR / "data", datDir)
|
||||||
|
print("Copied: data/*")
|
||||||
|
|
||||||
|
shutil.copyfile(SETUP_DIR / "description_short.txt", outDir / "data" / "description_short.txt")
|
||||||
|
print("Copied: data/description_short.txt")
|
||||||
|
|
||||||
|
# Build Package
|
||||||
|
# =============
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("Running dpkg-buildpackage ...")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
if signKey is None:
|
||||||
|
signArgs = ["-us", "-uc"]
|
||||||
|
else:
|
||||||
|
signArgs = [f"-k{signKey}"]
|
||||||
|
|
||||||
|
if sourceBuild:
|
||||||
|
subprocess.call(["debuild", "-S"] + signArgs, cwd=outDir)
|
||||||
|
toUpload(bldDir / f"{bldPkg}.tar.xz")
|
||||||
|
else:
|
||||||
|
subprocess.call(["dpkg-buildpackage"] + signArgs, cwd=outDir)
|
||||||
|
shutil.copyfile(bldDir / f"{bldPkg}.tar.xz", bldDir / f"{bldPkg}.debian.tar.xz")
|
||||||
|
toUpload(bldDir / f"{bldPkg}.debian.tar.xz")
|
||||||
|
toUpload(bldDir / f"{bldPkg}_all.deb")
|
||||||
|
toUpload(makeCheckSum(f"{bldPkg}.debian.tar.xz", cwd=bldDir))
|
||||||
|
toUpload(makeCheckSum(f"{bldPkg}_all.deb", cwd=bldDir))
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("Done!")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
if sourceBuild:
|
||||||
|
ppaName = "novelwriter" if hexVers[-2] == "f" else "novelwriter-pre"
|
||||||
|
return f"dput {ppaName}/{distName} {bldDir}/{bldPkg}_source.changes"
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def debian(args: argparse.Namespace) -> None:
|
||||||
|
"""Build a .deb package"""
|
||||||
|
if sys.platform == "linux":
|
||||||
|
print("ERROR: Command 'build-deb' can only be used on Linux")
|
||||||
|
sys.exit(1)
|
||||||
|
signKey = SIGN_KEY if args.sign else None
|
||||||
|
makeDebianPackage(signKey)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def launchpad(args: argparse.Namespace) -> None:
|
||||||
|
"""Wrapper for building Debian packages for Launchpad."""
|
||||||
|
if sys.platform == "linux":
|
||||||
|
print("ERROR: Command 'build-ubuntu' can only be used on Linux")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("Launchpad Packages")
|
||||||
|
print("==================")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
if args.build:
|
||||||
|
bldNum = str(args.build)
|
||||||
|
else:
|
||||||
|
bldNum = "0"
|
||||||
|
|
||||||
|
distLoop = [
|
||||||
|
("24.04", "noble"),
|
||||||
|
("24.10", "oracular"),
|
||||||
|
("25.04", "plucky"),
|
||||||
|
]
|
||||||
|
|
||||||
|
print("Building Ubuntu packages for:")
|
||||||
|
print("")
|
||||||
|
for distNum, codeName in distLoop:
|
||||||
|
print(f" * Ubuntu {distNum} {codeName.title()}")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
signKey = SIGN_KEY if args.sign else None
|
||||||
|
|
||||||
|
print(f"Sign Key: {str(signKey)}")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
dputCmd = []
|
||||||
|
for distNum, codeName in distLoop:
|
||||||
|
buildName = f"ubuntu{distNum}.{bldNum}"
|
||||||
|
dCmd = makeDebianPackage(
|
||||||
|
signKey=signKey,
|
||||||
|
sourceBuild=True,
|
||||||
|
distName=codeName,
|
||||||
|
buildName=buildName,
|
||||||
|
forLaunchpad=True,
|
||||||
|
)
|
||||||
|
dputCmd.append(dCmd)
|
||||||
|
|
||||||
|
print("Packages Built")
|
||||||
|
print("==============")
|
||||||
|
print("")
|
||||||
|
for dCmd in dputCmd:
|
||||||
|
print(f" > {dCmd}")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
return
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
"""
|
||||||
|
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, readFile, writeFile
|
||||||
|
|
||||||
|
|
||||||
|
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 deleteFolder(folder: Path) -> None:
|
||||||
|
if folder.is_dir():
|
||||||
|
shutil.rmtree(folder)
|
||||||
|
print(f"Deleted: {folder}")
|
||||||
|
|
||||||
|
def unlinkIfPrefix(folder: Path, prefix: tuple[str, ...]) -> None:
|
||||||
|
if folder.is_dir():
|
||||||
|
for item in folder.iterdir():
|
||||||
|
if item.name.startswith(prefix):
|
||||||
|
if item.is_file():
|
||||||
|
unlinkIfFound(item)
|
||||||
|
elif item.is_dir():
|
||||||
|
deleteFolder(item)
|
||||||
|
|
||||||
|
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(bindDir, 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
|
||||||
+182
@@ -0,0 +1,182 @@
|
|||||||
|
"""
|
||||||
|
novelWriter – Common Utils
|
||||||
|
==========================
|
||||||
|
|
||||||
|
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 shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
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 = ROOT_DIR / "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:
|
||||||
|
return version.partition("a")[0]
|
||||||
|
elif "b" in version:
|
||||||
|
return version.partition("b")[0]
|
||||||
|
elif "rc" in version:
|
||||||
|
return version.partition("rc")[0]
|
||||||
|
else:
|
||||||
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
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 copyPackageFiles(dst: Path, setupPy: bool = False) -> None:
|
||||||
|
"""Copy files needed for packaging."""
|
||||||
|
copyFiles = ["LICENSE.md", "CREDITS.md", "pyproject.toml"]
|
||||||
|
for copyFile in copyFiles:
|
||||||
|
shutil.copyfile(copyFile, dst / copyFile)
|
||||||
|
print("Copied: %s" % copyFile)
|
||||||
|
|
||||||
|
writeFile(dst / "MANIFEST.in", (
|
||||||
|
"include LICENSE.md\n"
|
||||||
|
"include CREDITS.md\n"
|
||||||
|
"recursive-include novelwriter/assets *\n"
|
||||||
|
))
|
||||||
|
print("Wrote: MANIFEST.in")
|
||||||
|
|
||||||
|
if setupPy:
|
||||||
|
writeFile(dst / "setup.py", (
|
||||||
|
"import setuptools\n"
|
||||||
|
"setuptools.setup()\n"
|
||||||
|
))
|
||||||
|
print("Wrote: setup.py")
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
uplDir = Path("dist_upload")
|
||||||
|
uplDir.mkdir(exist_ok=True)
|
||||||
|
srcPath = Path(srcPath)
|
||||||
|
shutil.copyfile(srcPath, uplDir / (dstName or srcPath.name))
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def makeCheckSum(sumFile: str, cwd: Path | None = None) -> str:
|
||||||
|
"""Create a SHA256 checksum file."""
|
||||||
|
try:
|
||||||
|
if cwd is None:
|
||||||
|
shaFile = f"{sumFile}.sha256"
|
||||||
|
else:
|
||||||
|
shaFile = cwd / f"{sumFile}.sha256"
|
||||||
|
with open(shaFile, mode="w") as fOut:
|
||||||
|
subprocess.call(["shasum", "-a", "256", sumFile], stdout=fOut, cwd=cwd)
|
||||||
|
print(f"SHA256 Sum: {shaFile}")
|
||||||
|
except Exception as exc:
|
||||||
|
print("Could not generate sha256 file")
|
||||||
|
print(str(exc))
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return str(shaFile)
|
||||||
|
|
||||||
|
|
||||||
|
def checkAssetsExist() -> bool:
|
||||||
|
"""Check that the necessary assets exist ahead of a build."""
|
||||||
|
hasSample = False
|
||||||
|
hasManual = False
|
||||||
|
hasQmData = False
|
||||||
|
|
||||||
|
sampleZip = ROOT_DIR / "novelwriter" / "assets" / "sample.zip"
|
||||||
|
if sampleZip.is_file():
|
||||||
|
print(f"Found: {sampleZip}")
|
||||||
|
hasSample = True
|
||||||
|
|
||||||
|
pdfManual = ROOT_DIR / "novelwriter" / "assets" / "manual.pdf"
|
||||||
|
if pdfManual.is_file():
|
||||||
|
print(f"Found: {pdfManual}")
|
||||||
|
hasManual = True
|
||||||
|
|
||||||
|
i18nAssets = ROOT_DIR / "novelwriter" / "assets" / "i18n"
|
||||||
|
if len(list(i18nAssets.glob("*.qm"))) > 0:
|
||||||
|
print(f"Found: {i18nAssets}/*.qm")
|
||||||
|
hasQmData = True
|
||||||
|
|
||||||
|
return hasSample and hasManual and hasQmData
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
+85
-1
@@ -20,12 +20,16 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
import json
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from xml.etree import ElementTree as ET
|
from xml.etree import ElementTree as ET
|
||||||
|
|
||||||
|
from utils.common import ROOT_DIR
|
||||||
|
|
||||||
UTILS = Path(__file__).parent
|
UTILS = Path(__file__).parent
|
||||||
ET.register_namespace("", "http://www.w3.org/2000/svg")
|
ET.register_namespace("", "http://www.w3.org/2000/svg")
|
||||||
ICONS = [
|
ICONS = [
|
||||||
@@ -140,7 +144,7 @@ ICONS = [
|
|||||||
|
|
||||||
def _loadMap(name: str) -> dict[str, str]:
|
def _loadMap(name: str) -> dict[str, str]:
|
||||||
"""Load a theme map file."""
|
"""Load a theme map file."""
|
||||||
data = json.loads((UTILS / f"{name}.json").read_text(encoding="utf-8"))
|
data = json.loads((UTILS / "icon_themes" / f"{name}.json").read_text(encoding="utf-8"))
|
||||||
icons = {}
|
icons = {}
|
||||||
for key in ICONS:
|
for key in ICONS:
|
||||||
if icon := data.get(key, ""):
|
if icon := data.get(key, ""):
|
||||||
@@ -311,3 +315,83 @@ def processRemix(workDir: Path, iconsDir: Path, jobs: dict) -> None:
|
|||||||
print("")
|
print("")
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def main(args: argparse.Namespace) -> None:
|
||||||
|
"""Build icon themes entry point."""
|
||||||
|
print("")
|
||||||
|
print("Build Icon Themes")
|
||||||
|
print("=================")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
workDir = Path(args.sources).absolute()
|
||||||
|
if not workDir.is_dir():
|
||||||
|
print(f"Source directory not found: {workDir}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
iconsDir = ROOT_DIR / "novelwriter" / "assets" / "icons"
|
||||||
|
|
||||||
|
style = args.style
|
||||||
|
if style in ("all", "material"):
|
||||||
|
processMaterialIcons(workDir, iconsDir, {
|
||||||
|
"material_rounded_thin": {
|
||||||
|
"name": "Material Symbols - Rounded Thin",
|
||||||
|
"style": "rounded",
|
||||||
|
"filled": False,
|
||||||
|
"weight": 200,
|
||||||
|
},
|
||||||
|
"material_rounded_normal": {
|
||||||
|
"name": "Material Symbols - Rounded Medium",
|
||||||
|
"style": "rounded",
|
||||||
|
"filled": False,
|
||||||
|
"weight": 400,
|
||||||
|
},
|
||||||
|
"material_rounded_bold": {
|
||||||
|
"name": "Material Symbols - Rounded Bold",
|
||||||
|
"style": "rounded",
|
||||||
|
"filled": False,
|
||||||
|
"weight": 600,
|
||||||
|
},
|
||||||
|
"material_filled_thin": {
|
||||||
|
"name": "Material Symbols - Filled Thin",
|
||||||
|
"style": "rounded",
|
||||||
|
"filled": True,
|
||||||
|
"weight": 200,
|
||||||
|
},
|
||||||
|
"material_filled_normal": {
|
||||||
|
"name": "Material Symbols - Filled Medium",
|
||||||
|
"style": "rounded",
|
||||||
|
"filled": True,
|
||||||
|
"weight": 400,
|
||||||
|
},
|
||||||
|
"material_filled_bold": {
|
||||||
|
"name": "Material Symbols - Filled Bold",
|
||||||
|
"style": "rounded",
|
||||||
|
"filled": True,
|
||||||
|
"weight": 600,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if style in ("all", "fa"):
|
||||||
|
processFontAwesome(workDir, iconsDir, {
|
||||||
|
"font_awesome": {
|
||||||
|
"name": "Font Awesome 6",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if style in ("all", "remix"):
|
||||||
|
processRemix(workDir, iconsDir, {
|
||||||
|
"remix_outline": {
|
||||||
|
"name": "Remix Icon - Outline",
|
||||||
|
"filled": False,
|
||||||
|
},
|
||||||
|
"remix_filled": {
|
||||||
|
"name": "Remix Icon - Filled",
|
||||||
|
"filled": True,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
print("Done")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user