Clean up pkgutils and update Windows installer (#2196)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
# Setup/Install
|
||||
/MANIFEST
|
||||
/build/
|
||||
/build_*/
|
||||
/deploy/
|
||||
/dist/
|
||||
/dist_*/
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# Meta
|
||||
meta:name = Font Awesome 6
|
||||
meta:author = Fonticons Inc
|
||||
meta:license = Font Awesome Free License
|
||||
meta:license = CC BY 4.0
|
||||
|
||||
# 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>
|
||||
|
||||
@@ -109,10 +109,10 @@ class Config:
|
||||
|
||||
self._appPath = Path(__file__).parent.absolute()
|
||||
self._appRoot = self._appPath.parent
|
||||
if self._appRoot.is_file():
|
||||
# novelWriter is packaged as a single file
|
||||
self._appRoot = self._appRoot.parent
|
||||
self._appPath = self._appRoot
|
||||
if getattr(sys, "frozen", False): # pragma: no cover
|
||||
# novelWriter is packaged as an exe
|
||||
self._appPath = Path(__file__).parent.parent.absolute()
|
||||
self._appRoot = self._appPath
|
||||
|
||||
# Runtime Settings and Variables
|
||||
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
|
||||
|
||||
[InstallDelete]
|
||||
Type: filesandordirs; Name: "{app}\lib\*"
|
||||
Type: filesandordirs; Name: "{app}\novelwriter\*"
|
||||
|
||||
[UninstallDelete]
|
||||
Type: filesandordirs; Name: "{app}\lib\*"
|
||||
Type: filesandordirs; Name: "{app}\novelwriter\*"
|
||||
|
||||
[Files]
|
||||
|
||||
@@ -84,12 +84,6 @@ def testBaseConfig_Constructor(monkeypatch):
|
||||
assert tstConf.osWindows is False
|
||||
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
|
||||
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
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from pathlib import Path
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
from utils.common import ROOT_DIR
|
||||
|
||||
UTILS = Path(__file__).parent
|
||||
ET.register_namespace("", "http://www.w3.org/2000/svg")
|
||||
ICONS = [
|
||||
@@ -140,7 +144,7 @@ ICONS = [
|
||||
|
||||
def _loadMap(name: str) -> dict[str, str]:
|
||||
"""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 = {}
|
||||
for key in ICONS:
|
||||
if icon := data.get(key, ""):
|
||||
@@ -311,3 +315,83 @@ def processRemix(workDir: Path, iconsDir: Path, jobs: dict) -> None:
|
||||
print("")
|
||||
|
||||
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