Merge release 2.7.1 into 2.8a0

This commit is contained in:
Veronica Berglyd Olsen
2025-06-10 00:10:20 +02:00
33 changed files with 2184 additions and 2150 deletions
+14 -3
View File
@@ -8,10 +8,13 @@ jobs:
buildLinux-AppImage: buildLinux-AppImage:
needs: buildAssets needs: buildAssets
runs-on: ubuntu-latest # Needs to stay on 22.04 as long as we're using manylinux_2_28
# as libxcb-cursor0 in 22.04 supports glibc >= 2.17
runs-on: ubuntu-22.04
env: env:
PYTHON_VERSION: "3.13" PYTHON_VERSION: "3.13"
LINUX_TAG: "manylinux_2_28_x86_64" LINUX_TAG: "manylinux_2_28"
LINUX_ARCH: "x86_64"
steps: steps:
- name: Python Setup - name: Python Setup
uses: actions/setup-python@v5 uses: actions/setup-python@v5
@@ -19,6 +22,11 @@ jobs:
python-version: "3.13" python-version: "3.13"
architecture: x64 architecture: x64
- name: Install Packages (apt)
run: |
sudo apt update
sudo apt install libxcb-cursor0
- name: Install Packages (pip) - name: Install Packages (pip)
run: pip install python-appimage setuptools run: pip install python-appimage setuptools
@@ -34,8 +42,11 @@ jobs:
- name: Build AppImage - name: Build AppImage
id: build id: build
run: | run: |
wget https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-$LINUX_ARCH.AppImage
chmod +x appimagetool-$LINUX_ARCH.AppImage
export APPIMAGE_TOOL_EXEC="$(pwd)/appimagetool-$LINUX_ARCH.AppImage"
echo "BUILD_VERSION=$(python pkgutils.py version)" >> $GITHUB_OUTPUT echo "BUILD_VERSION=$(python pkgutils.py version)" >> $GITHUB_OUTPUT
python pkgutils.py build-appimage --linux-tag $LINUX_TAG --python-version $PYTHON_VERSION python pkgutils.py build-appimage $LINUX_TAG $LINUX_ARCH $PYTHON_VERSION
- name: Upload Artifacts - name: Upload Artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
+33
View File
@@ -1,5 +1,38 @@
# novelWriter Changelog # novelWriter Changelog
## Version 2.7.1 [2025-06-09]
### Release Notes
This is a patch release that fixes some issues with tags and references, an issue with the
auto-complete menu on Windows, and some issues with the AppImage on Linux. The Czech translation
for 2.7 has also been completed.
### Detailed Changelog
**Bugfixes**
* Fix a bug where tags would be listed in the auto-complete menu for mentions even if the file
defining it had been moved to Archive or Trash. Issue #2387. PR #2389.
* Fixed a bug that seems to only have occurred on Windows where the auto-complete menu would hold
on to the return and arrow keys even when it was hidden. Issue #2386. PR #2389.
**Improvements**
* The auto-complete menu in the editor no longer shows any suggestions when defining the tag.
This never made sense anyway. PR #2389.
**Internationalisation**
* Updated the Czech. PR #2393.
**Packaging**
* Fixed an issue with the AppImage release where on some platforms `libxcb-cursor0` was missing.
The library is now included in the AppImage. Issue #2374. PR #2392.
----
## Version 2.7 [2025-06-01] ## Version 2.7 [2025-06-01]
### Release Notes ### Release Notes
+184 -184
View File
File diff suppressed because it is too large Load Diff
+235 -235
View File
File diff suppressed because it is too large Load Diff
+184 -184
View File
File diff suppressed because it is too large Load Diff
+184 -184
View File
File diff suppressed because it is too large Load Diff
+184 -184
View File
File diff suppressed because it is too large Load Diff
+184 -184
View File
File diff suppressed because it is too large Load Diff
+184 -184
View File
File diff suppressed because it is too large Load Diff
+184 -184
View File
File diff suppressed because it is too large Load Diff
+184 -184
View File
File diff suppressed because it is too large Load Diff
+184 -184
View File
File diff suppressed because it is too large Load Diff
@@ -3,6 +3,8 @@
"Short Description": "Krátký popis", "Short Description": "Krátký popis",
"Footnotes": "Poznámky pod čarou", "Footnotes": "Poznámky pod čarou",
"Comment": "Komentář", "Comment": "Komentář",
"Story Structure": "Struktura příběhu",
"Note": "Poznámka",
"Notes": "Poznámka", "Notes": "Poznámka",
"Tag": "Štítek", "Tag": "Štítek",
"Point of View": "Úhel pohledu", "Point of View": "Úhel pohledu",
+4
View File
@@ -185,6 +185,10 @@ class nwKeyWords:
POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY, WORLD_KEY, POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY, WORLD_KEY,
OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY, OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY,
] ]
CAN_LOOKUP: Final[list[str]] = [
POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY, WORLD_KEY,
OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY, STORY_KEY, MENTION_KEY,
]
# Set of Valid Keys # Set of Valid Keys
VALID_KEYS: Final[set[str]] = set(ALL_KEYS) VALID_KEYS: Final[set[str]] = set(ALL_KEYS)
+16 -10
View File
@@ -131,12 +131,6 @@ class Index:
self._novelExtra = extra self._novelExtra = extra
return return
def setItemClass(self, tHandle: str, itemClass: nwItemClass) -> None:
"""Update the class for all tags of a handle."""
logger.info("Updating class for '%s'", tHandle)
self._tagsIndex.updateClass(tHandle, itemClass.name)
return
## ##
# Public Methods # Public Methods
## ##
@@ -183,6 +177,16 @@ class Index:
self.scanText(tHandle, self._project.storage.getDocumentText(tHandle)) self.scanText(tHandle, self._project.storage.getDocumentText(tHandle))
return return
def refreshHandle(self, tHandle: str) -> None:
"""Update the class for all tags of a handle."""
if item := self._project.tree[tHandle]:
logger.info("Updating class for '%s'", tHandle)
if item.isInactiveClass():
self.deleteHandle(tHandle)
else:
self._tagsIndex.updateClass(tHandle, item.itemClass.name)
return
def indexChangedSince(self, checkTime: int | float) -> bool: def indexChangedSince(self, checkTime: int | float) -> bool:
"""Check if the index has changed since a given time.""" """Check if the index has changed since a given time."""
return self._indexChange > float(checkTime) return self._indexChange > float(checkTime)
@@ -753,10 +757,12 @@ class Index:
"""Return all tags used by a specific document.""" """Return all tags used by a specific document."""
return self._itemIndex.allItemTags(tHandle) if tHandle else [] return self._itemIndex.allItemTags(tHandle) if tHandle else []
def getClassTags(self, itemClass: nwItemClass | None) -> list[str]: def getKeyWordTags(self, keyWord: str) -> list[str]:
"""Return all tags based on itemClass.""" """Return all tags usable for a specific keyword."""
name = None if itemClass is None else itemClass.name if keyWord in nwKeyWords.CAN_LOOKUP:
return self._tagsIndex.filterTagNames(name) itemClass = nwKeyWords.KEY_CLASS.get(keyWord)
return self._tagsIndex.filterTagNames(itemClass.name if itemClass else None)
return []
def getTagsData( def getTagsData(
self, activeOnly: bool = True self, activeOnly: bool = True
+1 -1
View File
@@ -439,7 +439,7 @@ class NWItem:
self.setClass(itemClass) self.setClass(itemClass)
if self._type == nwItemType.FILE: if self._type == nwItemType.FILE:
# Notify the index of the class change # Notify the index of the class change
self._project.index.setItemClass(self._handle, itemClass) self._project.index.refreshHandle(self._handle)
if self._layout == nwItemLayout.NO_LAYOUT: if self._layout == nwItemLayout.NO_LAYOUT:
# If no layout is set, pick one # If no layout is set, pick one
+8 -7
View File
@@ -1090,11 +1090,14 @@ class GuiDocEditor(QPlainTextEdit):
show = self._completer.updateMetaText(text, bPos) show = self._completer.updateMetaText(text, bPos)
else: else:
show = self._completer.updateCommentText(text, bPos) show = self._completer.updateCommentText(text, bPos)
point = self.cursorRect().bottomRight() if show:
self._completer.move(viewport.mapToGlobal(point)) point = self.cursorRect().bottomRight()
self._completer.setVisible(show) self._completer.move(viewport.mapToGlobal(point))
self._completer.show()
else:
self._completer.close()
else: else:
self._completer.setVisible(False) self._completer.close()
if self._doReplace and added == 1: if self._doReplace and added == 1:
cursor = self.textCursor() cursor = self.textCursor()
@@ -2113,9 +2116,7 @@ class CommandCompleter(QMenu):
length = len(lookup) length = len(lookup)
suffix = "" suffix = ""
options = sorted(filter( options = sorted(filter(
lambda x: lookup in x.lower(), SHARED.project.index.getClassTags( lambda x: lookup in x.lower(), SHARED.project.index.getKeyWordTags(kw.strip())
nwKeyWords.KEY_CLASS.get(kw.strip())
)
))[:15] ))[:15]
if not options: if not options:
+6 -18
View File
@@ -272,24 +272,12 @@ if __name__ == "__main__":
cmdBuildUbuntu.set_defaults(func=utils.build_debian.launchpad) cmdBuildUbuntu.set_defaults(func=utils.build_debian.launchpad)
# Build AppImage # Build AppImage
cmdBuildAppImage = parsers.add_parser( # See https://github.com/pypa/manylinux
"build-appimage", help=( # See https://python-appimage.readthedocs.io/en/latest/#available-python-appimages
"Build an AppImage. " cmdBuildAppImage = parsers.add_parser("build-appimage", help="Build an AppImage.")
"Argument --linux-tag defaults manylinux_2_28_x86_64, and --python-version to 3.13." cmdBuildAppImage.add_argument("linux", help="Manylinux version, e.g. manylinux_2_28.")
) cmdBuildAppImage.add_argument("arch", help="Architecture, e.g. x86_64.")
) cmdBuildAppImage.add_argument("python", help="Python version, e.g. 3.13.")
cmdBuildAppImage.add_argument(
"--linux-tag",
default="manylinux_2_28_x86_64",
help=(
"Linux compatibility tag (e.g. manylinux_2_28_x86_64) "
"see https://python-appimage.readthedocs.io/en/latest/#available-python-appimages "
"and https://github.com/pypa/manylinux for a list of valid tags."
),
)
cmdBuildAppImage.add_argument(
"--python-version", default="3.13", help="Python version (e.g. 3.13)"
)
cmdBuildAppImage.set_defaults(func=utils.build_appimage.appImage) cmdBuildAppImage.set_defaults(func=utils.build_appimage.appImage)
# Build Windows Inno Setup Installer # Build Windows Inno Setup Installer
+2 -2
View File
@@ -7,9 +7,9 @@ name = "novelWriter"
authors = [ authors = [
{name = "Veronica Berglyd Olsen", email = "code@vkbo.net"}, {name = "Veronica Berglyd Olsen", email = "code@vkbo.net"},
] ]
description = "A markdown-like text editor for planning and writing novels" description = "A plain text editor for planning and writing novels"
readme = {file = "setup/description_pypi.md", content-type = "text/markdown"} readme = {file = "setup/description_pypi.md", content-type = "text/markdown"}
license = {text = "GNU General Public License v3"} license = "GPL-3.0"
classifiers = [ classifiers = [
"Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
+2 -2
View File
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.7" hexVersion="0x020700f0" fileVersion="1.5" fileRevision="5" timeStamp="2025-05-31 23:25:11"> <novelWriterXML appVersion="2.7.1" hexVersion="0x020701f0" fileVersion="1.5" fileRevision="5" timeStamp="2025-06-09 22:54:50">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="2193" autoCount="286" editTime="96887"> <project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="2194" autoCount="286" editTime="96892">
<name>Sample Project</name> <name>Sample Project</name>
<author>Jane Smith</author> <author>Jane Smith</author>
</project> </project>
+1 -1
View File
@@ -24,7 +24,7 @@ cat > $HOME/.local/share/applications/novelWriter.desktop <<EOF
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Name=novelWriter Name=novelWriter
Comment=A markdown-like text editor for planning and writing novels Comment=A plain text editor for planning and writing novels
Exec=${IMGPATH} %f Exec=${IMGPATH} %f
Icon=${ICONPATH} Icon=${ICONPATH}
Categories=Qt;Office;WordProcessor; Categories=Qt;Office;WordProcessor;
+1 -1
View File
@@ -1,7 +1,7 @@
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Name=novelWriter Name=novelWriter
Comment=A markdown-like text editor for planning and writing novels Comment=A plain text editor for planning and writing novels
Exec=novelwriter %f Exec=novelwriter %f
Icon=novelwriter Icon=novelwriter
Categories=Qt;Office;WordProcessor; Categories=Qt;Office;WordProcessor;
+1 -7
View File
@@ -10,10 +10,4 @@ X-Python3-Version: >= 3.10
Package: novelwriter Package: novelwriter
Architecture: all Architecture: all
Depends: ${misc:Depends}, ${python3:Depends}, python3 (>=3.10), python3-pyqt6 (>= 6.4), python3-pyqt6.qtsvg (>= 6.4), python3-enchant (>= 2.0) Depends: ${misc:Depends}, ${python3:Depends}, python3 (>=3.10), python3-pyqt6 (>= 6.4), python3-pyqt6.qtsvg (>= 6.4), python3-enchant (>= 2.0)
Description: A markdown-like text editor for planning and writing novels Description: A plain text editor for planning and writing novels
novelWriter is a plain text editor designed for writing novels assembled from
many smaller text documents. It uses a minimal formatting syntax inspired by
Markdown, and adds a meta data syntax for comments, synopsis, and
cross-referencing. It's designed to be a simple text editor that allows for
easy organisation of text and notes, using human readable text files as
storage for robustness.
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

+1 -1
View File
@@ -11,7 +11,7 @@
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>novelWriter</string> <string>novelWriter</string>
<key>CFBundleGetInfoString</key> <key>CFBundleGetInfoString</key>
<string>novelWriter: A markdown-like text editor for planning and writing novels.</string> <string>novelWriter: A plain text editor for planning and writing novels.</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string>novelwriter.icns</string> <string>novelwriter.icns</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
+4 -4
View File
@@ -174,7 +174,7 @@ rm lib/python3.*/site-packages/PyQt6/QtWebEngine* || true
rm -r lib/python3.*/site-packages/PyQt6/Qt6/translations/qtwebengine* || true rm -r lib/python3.*/site-packages/PyQt6/Qt6/translations/qtwebengine* || true
rm -r lib/python3.*/site-packages/PyQt6/Qt6/plugins/webview/libqtwebview* || true rm -r lib/python3.*/site-packages/PyQt6/Qt6/plugins/webview/libqtwebview* || true
# Remove unneeded QtQuick/Decaritive components # Remove unneeded QtQuick/Declarative components
rm lib/python3.*/site-packages/PyQt6/QtQml* || true rm lib/python3.*/site-packages/PyQt6/QtQml* || true
rm lib/python3.*/site-packages/PyQt6/QtQuick* || true rm lib/python3.*/site-packages/PyQt6/QtQuick* || true
rm lib/python3.*/site-packages/PyQt6/WebChannel* || true rm lib/python3.*/site-packages/PyQt6/WebChannel* || true
@@ -200,16 +200,16 @@ mkdir -p $RLS_DIR
# --- Create DMG -------------------------------------------------------------------------------- # # --- Create DMG -------------------------------------------------------------------------------- #
# Generate .dmg # Generate .dmg
echo "Packageing DMG ..." echo "Packaging DMG ..."
brew install create-dmg brew install create-dmg
create-dmg --volname "novelWriter $VERSION" --volicon $SRC_DIR/setup/macos/novelwriter.icns \ create-dmg --volname "novelWriter $VERSION" --volicon $SRC_DIR/setup/macos/novelwriter.icns \
--window-pos 200 120 --window-size 800 400 --icon-size 100 \ --window-pos 200 120 --window-size 800 400 --icon-size 100 \
--icon novelWriter.app 200 190 --hide-extension novelWriter.app \ --icon novelWriter.app 200 190 --hide-extension novelWriter.app \
--app-drop-link 600 185 $RLS_DIR/novelWriter-"${VERSION}"-$ARCH.dmg "$BUILD_DIR"/ --app-drop-link 600 185 $RLS_DIR/novelwriter-"${VERSION}"-$ARCH.dmg "$BUILD_DIR"/
pushd $RLS_DIR || exit 1 pushd $RLS_DIR || exit 1
shasum -a 256 novelWriter-"${VERSION}"-$ARCH.dmg | tee novelWriter-"${VERSION}"-$ARCH.dmg.sha256 shasum -a 256 novelwriter-"${VERSION}"-$ARCH.dmg | tee novelwriter-"${VERSION}"-$ARCH.dmg.sha256
popd || exit 1 popd || exit 1
rm -r $CONDA_PATH rm -r $CONDA_PATH
+32 -8
View File
@@ -95,12 +95,35 @@ def testCoreIndex_LoadSave(qtbot, monkeypatch, prjLipsum, nwGUI, tstPaths):
tagIndex = str(index._tagsIndex.packData()) tagIndex = str(index._tagsIndex.packData())
itemsIndex = str(index._itemIndex.packData()) itemsIndex = str(index._itemIndex.packData())
# Update item class
bHandle = "4c4f28287af27"
bItem = project.tree[bHandle]
assert bItem is not None
tagsBod = index._tagsIndex["Bod"]
assert tagsBod is not None
assert tagsBod["handle"] == bHandle
assert tagsBod["class"] == "CHARACTER"
bItem.setClass(nwItemClass.CUSTOM)
index.refreshHandle(bHandle)
assert tagsBod is not None
assert tagsBod["handle"] == bHandle
assert tagsBod["class"] == "CUSTOM"
# Update item class to inactive
bItem.setClass(nwItemClass.TRASH)
index.refreshHandle(bHandle)
assert "Bod" not in index._tagsIndex
bItem.setClass(nwItemClass.CHARACTER)
index.reIndexHandle(bHandle)
# Delete a handle # Delete a handle
assert index._tagsIndex["Bod"] is not None assert index._tagsIndex["Bod"] is not None
assert index._itemIndex["4c4f28287af27"] is not None assert index._itemIndex[bHandle] is not None
index.deleteHandle("4c4f28287af27") index.deleteHandle(bHandle)
assert index._tagsIndex["Bod"] is None assert index._tagsIndex["Bod"] is None
assert index._itemIndex["4c4f28287af27"] is None assert index._itemIndex[bHandle] is None
# Clear the index # Clear the index
index.clear() index.clear()
@@ -766,11 +789,12 @@ def testCoreIndex_ExtractData(nwGUI, fncPath, mockRnd):
assert index.getDocumentTags(cHandle) == ["jane"] assert index.getDocumentTags(cHandle) == ["jane"]
assert index.getDocumentTags(None) == [] assert index.getDocumentTags(None) == []
# getClassTags # getKeyWordTags
# ============ # ==============
assert index.getClassTags(None) == ["Jane", "John"] assert index.getKeyWordTags("@mention") == ["Jane", "John"]
assert index.getClassTags(nwItemClass.CHARACTER) == ["Jane", "John"] assert index.getKeyWordTags("@char") == ["Jane", "John"]
assert index.getClassTags(nwItemClass.PLOT) == [] assert index.getKeyWordTags("@plot") == []
assert index.getKeyWordTags("@tag") == []
# getTagsData # getTagsData
# =========== # ===========
+46 -71
View File
@@ -22,13 +22,16 @@ from __future__ import annotations
import argparse import argparse
import datetime import datetime
import os
import shutil import shutil
import subprocess
import sys import sys
from pathlib import Path
from utils.common import ( from utils.common import (
ROOT_DIR, SETUP_DIR, appdataXml, copyPackageFiles, copySourceCode, ROOT_DIR, SETUP_DIR, appdataXml, copyPackageFiles, copySourceCode,
extractVersion, makeCheckSum, toUpload, writeFile extractVersion, freshFolder, makeCheckSum, removeRedundantQt, systemCall,
toUpload, writeFile
) )
@@ -49,115 +52,87 @@ def appImage(args: argparse.Namespace) -> None:
print("") print("")
print("Build AppImage") print("Build AppImage")
print("==============") print("="*120)
print("")
linuxTag = args.linux_tag mLinux = args.linux
pythonVer = args.python_version mArch = args.arch
pyVer = args.python
# Version Info # Version Info
# ============
pkgVers, _, relDate = extractVersion() pkgVers, _, relDate = extractVersion()
relDate = datetime.datetime.strptime(relDate, "%Y-%m-%d") relDate = datetime.datetime.strptime(relDate, "%Y-%m-%d")
print("")
# Set Up Folder # Set Up Folder
# =============
bldDir = ROOT_DIR / "dist_appimage" bldDir = ROOT_DIR / "dist_appimage"
bldPkg = f"novelwriter_{pkgVers}" bldPkg = f"novelwriter-{pkgVers}-{mArch}"
bldImg = f"{bldPkg}.AppImage"
outDir = bldDir / bldPkg outDir = bldDir / bldPkg
imgDir = bldDir / "appimage" imgDir = bldDir / "appimage"
appDir = bldDir / f"novelWriter-{mArch}"
# Set Up Folders
# ==============
bldDir.mkdir(exist_ok=True) bldDir.mkdir(exist_ok=True)
freshFolder(outDir)
if outDir.exists(): freshFolder(imgDir)
print("Removing old build files ...") freshFolder(appDir)
print("")
shutil.rmtree(outDir)
outDir.mkdir()
if imgDir.exists():
print("Removing old build metadata files ...")
print("")
shutil.rmtree(imgDir)
imgDir.mkdir()
# Remove old AppImages # Remove old AppImages
if images := bldDir.glob("*.AppImage"): if images := bldDir.glob("*.AppImage"):
print("Removing old AppImages") print("Removing old AppImages")
print("")
for image in images: for image in images:
image.unlink() image.unlink()
# Copy novelWriter Source # Copy novelWriter Source
# =======================
print("Copying novelWriter source ...") print("Copying novelWriter source ...")
print("")
copySourceCode(outDir) copySourceCode(outDir)
print("")
print("Copying or generating additional files ...") print("Copying or generating additional files ...")
print("")
copyPackageFiles(outDir) copyPackageFiles(outDir)
# Write Metadata # Write Metadata
# ==============
writeFile(imgDir / "novelwriter.appdata.xml", appdataXml()) writeFile(imgDir / "novelwriter.appdata.xml", appdataXml())
print("Wrote: novelwriter.appdata.xml") writeFile(imgDir / "requirements.txt", str(outDir))
writeFile(imgDir / "entrypoint.sh", ( writeFile(imgDir / "entrypoint.sh", (
'#! /bin/bash \n' # f"export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${{APPDIR}}/usr/lib/{mArch}-linux-gnu/\n"
'{{ python-executable }} -sE ${APPDIR}/opt/python{{ python-version }}/bin/novelwriter "$@"' '{{ 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") shutil.copyfile(SETUP_DIR / "data" / "novelwriter.desktop", imgDir / "novelwriter.desktop")
print("Copied: novelwriter.desktop") print("Copied: novelwriter.desktop")
shutil.copyfile(SETUP_DIR / "icons" / "novelwriter.svg", imgDir / "novelwriter.svg") shutil.copyfile(SETUP_DIR / "icons" / "novelwriter.png", imgDir / "novelwriter.png")
print("Copied: novelwriter.svg")
shutil.copyfile(
SETUP_DIR / "data" / "hicolor" / "256x256" / "apps" / "novelwriter.png",
imgDir / "novelwriter.png"
)
print("Copied: novelwriter.png") print("Copied: novelwriter.png")
# Build AppImage # Build AppDir
# ============== systemCall([
sys.executable, "-m", "python_appimage", "build", "app", "--no-packaging",
"-l", f"{mLinux}_{mArch}", "-p", pyVer, "appimage"
], cwd=bldDir)
try: # Copy Libraries
subprocess.call([ libPath = Path(f"/usr/lib/{mArch}-linux-gnu")
sys.executable, "-m", "python_appimage", "build", "app", siteDir = appDir / "opt" / f"python{pyVer}" / "lib" / f"python{pyVer}" / "site-packages"
"-l", linuxTag, "-p", pythonVer, "appimage" qt6Lib = siteDir / "PyQt6" / "Qt6" / "lib"
], cwd=bldDir) shutil.copyfile(libPath / "libxcb-cursor.so.0", qt6Lib / "libxcb-cursor.so.0")
except Exception as exc:
print("AppImage build: FAILED")
print("")
print(str(exc))
print("")
sys.exit(1)
bldFile = list(bldDir.glob("*.AppImage"))[0] # Remove Redundant
outFile = bldDir / f"novelWriter-{pkgVers}.AppImage" removeRedundantQt(siteDir)
bldFile.rename(outFile)
shaFile = makeCheckSum(outFile.name, cwd=bldDir)
toUpload(outFile) # Build Image
appToolExec = os.environ.get("APPIMAGE_TOOL_EXEC", "appimagetool")
env = os.environ.copy()
env["ARCH"] = mArch
systemCall([
appToolExec, "--no-appstream", "--updateinformation",
f"gh-releases-zsync|vkbo|novelwriter|latest|novelwriter-*-{mArch}.AppImage.zsync",
str(appDir), bldImg
], cwd=bldDir, env=env)
updFile = bldDir / f"{bldImg}.zsync"
bldFile = bldDir / bldImg
shaFile = makeCheckSum(bldFile.name, cwd=bldDir)
toUpload(bldFile)
toUpload(updFile)
toUpload(shaFile) toUpload(shaFile)
return return
+4 -4
View File
@@ -24,12 +24,11 @@ import argparse
import datetime import datetime
import email.utils import email.utils
import shutil import shutil
import subprocess
import sys import sys
from utils.common import ( from utils.common import (
ROOT_DIR, SETUP_DIR, checkAssetsExist, copyPackageFiles, copySourceCode, ROOT_DIR, SETUP_DIR, checkAssetsExist, copyPackageFiles, copySourceCode,
extractVersion, makeCheckSum, toUpload, writeFile extractVersion, makeCheckSum, systemCall, toUpload, writeFile
) )
SIGN_KEY = "D6A9F6B8F227CF7C6F6D1EE84DBBE4B734B0BD08" SIGN_KEY = "D6A9F6B8F227CF7C6F6D1EE84DBBE4B734B0BD08"
@@ -134,10 +133,10 @@ def makeDebianPackage(
signArgs = [f"-k{signKey}"] signArgs = [f"-k{signKey}"]
if sourceBuild: if sourceBuild:
subprocess.call(["debuild", "-S", *signArgs], cwd=outDir) systemCall(["debuild", "-S", *signArgs], cwd=outDir)
toUpload(bldDir / f"{bldPkg}.tar.xz") toUpload(bldDir / f"{bldPkg}.tar.xz")
else: else:
subprocess.call(["dpkg-buildpackage", *signArgs], cwd=outDir) systemCall(["dpkg-buildpackage", *signArgs], cwd=outDir)
shutil.copyfile(bldDir / f"{bldPkg}.tar.xz", bldDir / f"{bldPkg}.debian.tar.xz") shutil.copyfile(bldDir / f"{bldPkg}.tar.xz", bldDir / f"{bldPkg}.debian.tar.xz")
toUpload(bldDir / f"{bldPkg}.debian.tar.xz") toUpload(bldDir / f"{bldPkg}.debian.tar.xz")
toUpload(bldDir / f"{bldPkg}_all.deb") toUpload(bldDir / f"{bldPkg}_all.deb")
@@ -185,6 +184,7 @@ def launchpad(args: argparse.Namespace) -> None:
("24.04", "noble"), ("24.04", "noble"),
("24.10", "oracular"), ("24.10", "oracular"),
("25.04", "plucky"), ("25.04", "plucky"),
("25.10", "questing"),
] ]
print("Building Ubuntu packages for:") print("Building Ubuntu packages for:")
+8 -99
View File
@@ -23,14 +23,16 @@ from __future__ import annotations
import argparse import argparse
import compileall import compileall
import shutil import shutil
import subprocess
import sys import sys
import urllib.request import urllib.request
import zipfile import zipfile
from pathlib import Path from pathlib import Path
from utils.common import ROOT_DIR, SETUP_DIR, copySourceCode, extractVersion, readFile, writeFile from utils.common import (
ROOT_DIR, SETUP_DIR, copySourceCode, extractVersion, readFile,
removeRedundantQt, systemCall, writeFile
)
def prepareCode(outDir: Path) -> None: def prepareCode(outDir: Path) -> None:
@@ -86,99 +88,11 @@ def embedPython(bldDir: Path, outDir: Path) -> None:
def installRequirements(libDir: Path) -> None: def installRequirements(libDir: Path) -> None:
"""Install dependencies.""" """Install dependencies."""
print("Install dependencies ...") print("Install dependencies ...")
systemCall([
try: sys.executable, "-m", "pip", "install", "-r", "requirements.txt", "--target", libDir
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("Done")
print("") 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 return
@@ -238,12 +152,7 @@ def main(args: argparse.Namespace) -> None:
writeFile(ROOT_DIR / "setup.iss", issData) writeFile(ROOT_DIR / "setup.iss", issData)
print("") print("")
try: systemCall(["iscc", "setup.iss"])
subprocess.call(["iscc", "setup.iss"])
except Exception as exc:
print("Inno Setup failed with error:")
print(str(exc))
sys.exit(1)
print("") print("")
print("Done") print("Done")
+104 -17
View File
@@ -22,6 +22,7 @@ from __future__ import annotations
import shutil import shutil
import subprocess import subprocess
import sys
from pathlib import Path from pathlib import Path
@@ -50,11 +51,11 @@ def extractVersion(beQuiet: bool = False) -> tuple[str, str, str]:
if aLine.startswith("__date__"): if aLine.startswith("__date__"):
relDate = getValue(aLine) relDate = getValue(aLine)
except Exception as exc: except Exception as exc:
print(f"Could not read file: {initFile}") print(f"Could not read file: {initFile}", flush=True)
print(str(exc)) print(str(exc), flush=True)
if not beQuiet: if not beQuiet:
print(f"novelWriter version: {numVers} ({hexVers}) at {relDate}") print(f"novelWriter version: {numVers} ({hexVers}) at {relDate}", flush=True)
return numVers, hexVers, relDate return numVers, hexVers, relDate
@@ -77,16 +78,16 @@ def copySourceCode(dst: Path) -> None:
for item in src.glob("**/*"): for item in src.glob("**/*"):
relSrc = item.relative_to(ROOT_DIR) relSrc = item.relative_to(ROOT_DIR)
if item.suffix in (".pyc", ".pyo"): if item.suffix in (".pyc", ".pyo"):
print(f"Ignore: {relSrc}") print("Ignored:", relSrc, flush=True)
continue continue
if item.parent.is_dir() and item.parent.name != "__pycache__": if item.parent.is_dir() and item.parent.name != "__pycache__":
dstDir = dst / relSrc.parent dstDir = dst / relSrc.parent
if not dstDir.exists(): if not dstDir.exists():
dstDir.mkdir(parents=True) dstDir.mkdir(parents=True)
print(f"Folder: {dstDir}") print("Created:", dstDir.relative_to(ROOT_DIR), flush=True)
if item.is_file(): if item.is_file():
shutil.copyfile(item, dst / relSrc) shutil.copyfile(item, dst / relSrc)
print(f"Copied: {dst / relSrc}") print("Copied:", relSrc, flush=True)
return return
@@ -95,26 +96,23 @@ def copyPackageFiles(dst: Path, setupPy: bool = False) -> None:
copyFiles = ["LICENSE.md", "CREDITS.md", "pyproject.toml"] copyFiles = ["LICENSE.md", "CREDITS.md", "pyproject.toml"]
for copyFile in copyFiles: for copyFile in copyFiles:
shutil.copyfile(copyFile, dst / copyFile) shutil.copyfile(copyFile, dst / copyFile)
print(f"Copied: {copyFile}") print("Copied:", copyFile, flush=True)
writeFile(dst / "MANIFEST.in", ( writeFile(dst / "MANIFEST.in", (
"include LICENSE.md\n" "include LICENSE.md\n"
"include CREDITS.md\n" "include CREDITS.md\n"
"recursive-include novelwriter/assets *\n" "recursive-include novelwriter/assets *\n"
)) ))
print("Wrote: MANIFEST.in")
if setupPy: if setupPy:
writeFile(dst / "setup.py", ( writeFile(dst / "setup.py", (
"import setuptools\n" "import setuptools\n"
"setuptools.setup()\n" "setuptools.setup()\n"
)) ))
print("Wrote: setup.py")
text = readFile(ROOT_DIR / "pyproject.toml") text = readFile(ROOT_DIR / "pyproject.toml")
text = text.replace("setup/description_pypi.md", "data/description_short.txt") text = text.replace("setup/description_pypi.md", "data/description_short.txt")
writeFile(dst / "pyproject.toml", text) writeFile(dst / "pyproject.toml", text)
print("Wrote: pyproject.toml")
return return
@@ -139,10 +137,10 @@ def makeCheckSum(sumFile: str, cwd: Path | None = None) -> str:
shaFile = cwd / f"{sumFile}.sha256" shaFile = cwd / f"{sumFile}.sha256"
with open(shaFile, mode="w", encoding="utf-8") as fOut: with open(shaFile, mode="w", encoding="utf-8") as fOut:
subprocess.call(["shasum", "-a", "256", sumFile], stdout=fOut, cwd=cwd) subprocess.call(["shasum", "-a", "256", sumFile], stdout=fOut, cwd=cwd)
print(f"SHA256 Sum: {shaFile}") print(f"SHA256 Sum: {shaFile}", flush=True)
except Exception as exc: except Exception as exc:
print("Could not generate sha256 file") print("Could not generate sha256 file", flush=True)
print(str(exc)) print(str(exc), flush=True)
return "" return ""
return str(shaFile) return str(shaFile)
@@ -156,17 +154,17 @@ def checkAssetsExist() -> bool:
sampleZip = ROOT_DIR / "novelwriter" / "assets" / "sample.zip" sampleZip = ROOT_DIR / "novelwriter" / "assets" / "sample.zip"
if sampleZip.is_file(): if sampleZip.is_file():
print(f"Found: {sampleZip}") print(f"Found: {sampleZip}", flush=True)
hasSample = True hasSample = True
pdfManual = ROOT_DIR / "novelwriter" / "assets" / "manual.pdf" pdfManual = ROOT_DIR / "novelwriter" / "assets" / "manual.pdf"
if pdfManual.is_file(): if pdfManual.is_file():
print(f"Found: {pdfManual}") print(f"Found: {pdfManual}", flush=True)
hasManual = True hasManual = True
i18nAssets = ROOT_DIR / "novelwriter" / "assets" / "i18n" i18nAssets = ROOT_DIR / "novelwriter" / "assets" / "i18n"
if len(list(i18nAssets.glob("*.qm"))) > 0: if len(list(i18nAssets.glob("*.qm"))) > 0:
print(f"Found: {i18nAssets}/*.qm") print(f"Found: {i18nAssets}/*.qm", flush=True)
hasQmData = True hasQmData = True
return hasSample and hasManual and hasQmData return hasSample and hasManual and hasQmData
@@ -188,4 +186,93 @@ def readFile(file: Path) -> str:
def writeFile(file: Path, text: str) -> int: def writeFile(file: Path, text: str) -> int:
"""Write string to file.""" """Write string to file."""
return file.write_text(text, encoding="utf-8") result = file.write_text(text, encoding="utf-8")
print("Wrote:", file.relative_to(ROOT_DIR), flush=True)
return result
def freshFolder(path: Path) -> None:
"""Make sure a folder exists and is empty."""
if path.exists():
print("Removing:", str(path), flush=True)
shutil.rmtree(path)
path.mkdir()
return
def systemCall(cmd: list, cwd: Path | str | None = None, env: dict | None = None) -> int:
"""Make a system call using subprocess."""
if isinstance(cwd, Path):
cwd = str(cwd)
try:
code = subprocess.call([str(c) for c in cmd], cwd=cwd, env=env)
except Exception as exc:
print("ERROR:", str(exc), flush=True)
sys.exit(1)
return code
def removeRedundantQt(qtBase: Path) -> None:
"""Delete Qt files that are not needed"""
def unlinkIfFound(file: Path) -> None:
if file.is_file():
file.unlink()
print("Deleted:", file.relative_to(ROOT_DIR), flush=True)
def deleteFolder(folder: Path) -> None:
if folder.is_dir():
shutil.rmtree(folder)
print("Deleted:", folder.relative_to(ROOT_DIR), flush=True)
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 ...")
pyQt6Dir = qtBase / "PyQt6"
bindDir = qtBase / "PyQt6" / "bindings"
qt6Dir = qtBase / "PyQt6" / "Qt6"
binDir = qtBase / "PyQt6" / "Qt6" / "bin"
libDir = qtBase / "PyQt6" / "Qt6" / "lib"
plugDir = qtBase / "PyQt6" / "Qt6" / "plugins"
qmDir = qtBase / "PyQt6" / "Qt6" / "translations"
dictDir = qtBase / "enchant" / "data" / "mingw64" / "share" / "enchant" / "hunspell"
# Prune Dictionaries
if dictDir.exists():
for item in dictDir.iterdir():
if not item.name.startswith(("en_GB", "en_US")):
unlinkIfFound(item)
# Prune Translations
for item in qmDir.iterdir():
if not item.name.startswith("qtbase"):
unlinkIfFound(item)
# Delete Modules
modules = [
"Qt6Qml", "Qt6Quick", "Qt6Bluetooth", "Qt6Nfc",
"Qt6Sensors", "Qt6SerialPort", "Qt6Test",
]
modules.extend([x.replace("Qt6", "Qt") for x in modules])
modules.extend([f"lib{x}" for x in modules])
modules = tuple(modules)
unlinkIfPrefix(pyQt6Dir, modules)
unlinkIfPrefix(bindDir, modules)
unlinkIfPrefix(binDir, modules)
unlinkIfPrefix(libDir, modules)
# Other Files
deleteFolder(qt6Dir / "qml")
deleteFolder(plugDir / "qmlls")
deleteFolder(plugDir / "qmllint")
return
+3 -3
View File
@@ -25,7 +25,7 @@ import os
import shutil import shutil
import subprocess import subprocess
from utils.common import ROOT_DIR from utils.common import ROOT_DIR, systemCall
def updateDocsTranslationSources(args: argparse.Namespace) -> None: def updateDocsTranslationSources(args: argparse.Namespace) -> None:
@@ -40,7 +40,7 @@ def updateDocsTranslationSources(args: argparse.Namespace) -> None:
locsDir.mkdir(exist_ok=True) locsDir.mkdir(exist_ok=True)
print("Generating POT Files") print("Generating POT Files")
subprocess.call(["make", "gettext"], cwd=docsDir) systemCall(["make", "gettext"], cwd=docsDir)
print("") print("")
lang = args.lang lang = args.lang
@@ -55,7 +55,7 @@ def updateDocsTranslationSources(args: argparse.Namespace) -> None:
print("") print("")
for code in update: for code in update:
subprocess.call(["sphinx-intl", "update", "-p", "build/gettext", "-l", code], cwd=docsDir) systemCall(["sphinx-intl", "update", "-p", "build/gettext", "-l", code], cwd=docsDir)
print("") print("")
print("Done") print("Done")