Update all assets commands to pkgutils

This commit is contained in:
Veronica Berglyd Olsen
2024-07-04 18:34:10 +02:00
parent b91a11980b
commit 1b8867537e
4 changed files with 303 additions and 270 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ jobs:
buildAssets: buildAssets:
uses: ./.github/workflows/build_assets.yml uses: ./.github/workflows/build_assets.yml
buildLinux: buildLinux-AppImage:
needs: buildAssets needs: buildAssets
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
+300 -266
View File
@@ -25,6 +25,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from __future__ import annotations from __future__ import annotations
import argparse
import datetime import datetime
import email.utils import email.utils
import os import os
@@ -40,6 +41,8 @@ OS_LINUX = 1
OS_WIN = 2 OS_WIN = 2
OS_DARWIN = 3 OS_DARWIN = 3
CURR_DIR = Path(__file__).parent
# =============================================================================================== # # =============================================================================================== #
# Utilities # Utilities
@@ -131,11 +134,21 @@ def makeCheckSum(sumFile: str, cwd: str | None = None) -> str:
# General # General
# =============================================================================================== # # =============================================================================================== #
##
# Print Version
##
def printVersion(args: argparse.Namespace) -> None:
"""Print the novelWriter version and exit."""
print(extractVersion(beQuiet=True)[0], end=None)
return
## ##
# Package Installer (pip) # Package Installer (pip)
## ##
def installPackages(hostOS: int) -> None: def installPackages(args: argparse.Namespace) -> None:
"""Install package dependencies both for this script and for running """Install package dependencies both for this script and for running
novelWriter itself. novelWriter itself.
""" """
@@ -145,9 +158,9 @@ def installPackages(hostOS: int) -> None:
print("") print("")
installQueue = ["pip", "-r requirements.txt"] installQueue = ["pip", "-r requirements.txt"]
if hostOS == OS_DARWIN: if args.mac:
installQueue.append("pyobjc") installQueue.append("pyobjc")
elif hostOS == OS_WIN: elif args.win:
installQueue.append("pywin32") installQueue.append("pywin32")
pyCmd = [sys.executable, "-m"] pyCmd = [sys.executable, "-m"]
@@ -168,28 +181,30 @@ def installPackages(hostOS: int) -> None:
# Clean Build and Dist Folders (build-clean) # Clean Build and Dist Folders (build-clean)
## ##
def cleanBuildDirs() -> None: def cleanBuildDirs(args: argparse.Namespace) -> None:
"""Recursively delete the 'build' and 'dist' folders.""" """Recursively delete the 'build' and 'dist' folders."""
print("") print("")
print("Cleaning up build environment ...") print("Cleaning up build environment ...")
print("") print("")
def removeFolder(rmDir: str) -> None: folders = [
if os.path.isdir(rmDir): CURR_DIR / "build",
try: CURR_DIR / "dist",
shutil.rmtree(rmDir) CURR_DIR / "dist_deb",
print("Deleted: %s" % rmDir) CURR_DIR / "dist_minimal",
except OSError: CURR_DIR / "dist_appimage",
print("Failed: %s" % rmDir) CURR_DIR / "novelWriter.egg-info",
else: ]
print("Missing: %s" % rmDir)
removeFolder("build") for folder in folders:
removeFolder("dist") if folder.is_dir():
removeFolder("dist_deb") try:
removeFolder("dist_minimal") shutil.rmtree(folder)
removeFolder("dist_appimage") print("Deleted: %s" % folder)
removeFolder("novelWriter.egg-info") except OSError:
print("Failed: %s" % folder)
else:
print("Missing: %s" % folder)
print("") print("")
@@ -204,28 +219,23 @@ def cleanBuildDirs() -> None:
# Build PDF Manual (manual) # Build PDF Manual (manual)
## ##
def buildPdfManual() -> None: def buildPdfManual(args: argparse.Namespace | None = None) -> None:
"""This function will build the documentation as manual.pdf.""" """This function will build the documentation as manual.pdf."""
print("") print("")
print("Building PDF Manual") print("Building PDF Manual")
print("===================") print("===================")
print("") print("")
buildFile = os.path.join("docs", "build", "latex", "manual.pdf") buildFile = CURR_DIR / "docs" / "build" / "latex" / "manual.pdf"
finalFile = os.path.join("novelwriter", "assets", "manual.pdf") finalFile = CURR_DIR / "novelwriter" / "assets" / "manual.pdf"
finalFile.unlink(missing_ok=True)
if os.path.isfile(finalFile):
# Make sure a new file is always generated
os.unlink(finalFile)
try: try:
subprocess.call(["make", "clean"], cwd="docs") subprocess.call(["make", "clean"], cwd="docs")
exCode = subprocess.call(["make", "latexpdf"], cwd="docs") exCode = subprocess.call(["make", "latexpdf"], cwd="docs")
if exCode == 0: if exCode == 0:
if os.path.isfile(finalFile):
os.unlink(finalFile)
print("") print("")
os.rename(buildFile, finalFile) buildFile.rename(finalFile)
else: else:
raise Exception(f"Build returned error code {exCode}") raise Exception(f"Build returned error code {exCode}")
@@ -255,53 +265,36 @@ def buildPdfManual() -> None:
## ##
# Qt Linguist QM Builder (qtlrelease) # Sample Project ZIP File Builder (sample)
## ##
def buildQtI18n() -> None: def buildSampleZip(args: argparse.Namespace | None = None) -> None:
"""Build the lang.qm files for Qt Linguist.""" """Bundle the sample project into a single zip file to be saved into
the novelwriter/assets folder for further bundling into builds.
"""
print("") print("")
print("Building Qt Localisation Files") print("Building Sample ZIP File")
print("==============================") print("========================")
print("")
print("TS Files to Build:")
print("") print("")
tsList = [] srcSample = CURR_DIR / "sample"
for aFile in os.listdir("i18n"): dstSample = CURR_DIR / "novelwriter" / "assets" / "sample.zip"
aPath = os.path.join("i18n", aFile)
if os.path.isfile(aPath) and aFile.endswith(".ts") and aFile != "nw_base.ts":
tsList.append(aPath)
print(aPath)
print("") if srcSample.is_dir():
print("Building Translation Files:") dstSample.unlink(missing_ok=True)
print("") 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}")
try: else:
subprocess.call(["lrelease", "-verbose", *tsList]) print("Error: Could not find sample project source directory.")
except Exception as exc:
print("Qt5 Linguist tools seem to be missing")
print("On Debian/Ubuntu, install: qttools5-dev-tools pyqt5-dev-tools")
print(str(exc))
sys.exit(1) sys.exit(1)
print("") print("")
print("Moving QM Files to Assets") print("Built file: %s" % dstSample)
print("")
langDir = os.path.join("novelwriter", "assets", "i18n")
for langFile in os.listdir("i18n"):
langPath = os.path.join("i18n", langFile)
if not os.path.isfile(langPath):
continue
if langFile.endswith(".qm"):
destPath = os.path.join(langDir, langFile)
os.rename(langPath, destPath)
print("Moved: %s -> %s" % (langPath, destPath))
print("") print("")
return return
@@ -311,13 +304,14 @@ def buildQtI18n() -> None:
# Qt Linguist TS Builder (qtlupdate) # Qt Linguist TS Builder (qtlupdate)
## ##
def buildQtI18nTS(sysArgs: list[str]) -> None: def updateTranslationSources(args: argparse.Namespace) -> None:
"""Build the lang.ts files for Qt Linguist.""" """Build the lang.ts files for Qt Linguist."""
print("") print("")
print("Building Qt Translation Files") print("Building Qt Translation Files")
print("=============================") print("=============================")
try: try:
# Using the pylupdate tool from PyQt6 as it supports TS file format 2.1.
from PyQt6.lupdate.lupdate import lupdate from PyQt6.lupdate.lupdate import lupdate
except ImportError: except ImportError:
print("ERROR: This command requires lupdate from PyQt6") print("ERROR: This command requires lupdate from PyQt6")
@@ -328,65 +322,141 @@ def buildQtI18nTS(sysArgs: list[str]) -> None:
print("Scanning Source Tree:") print("Scanning Source Tree:")
print("") print("")
sources = [os.path.join("i18n", "qtbase.py")] sources = list((CURR_DIR / "novelwriter").glob("**/*.py"))
for root, _, files in os.walk("novelwriter"): sources.insert(0, CURR_DIR / "i18n" / "qtbase.py")
if os.path.isdir(root):
for file in files:
source = os.path.join(root, file)
if os.path.isfile(source) and file.endswith(".py"):
sources.append(source)
for source in sources: for source in sources:
print(source) print(source.relative_to(CURR_DIR))
print("") print("")
print("TS Files to Update:") print("TS Files to Update:")
print("") print("")
translations = [] translations = []
if len(sysArgs) >= 2: for item in [Path(str(f)).absolute() for f in args.files]:
for arg in sysArgs[1:]: if not (item.name.startswith("nw_") and item.suffix == ".ts"):
if not (arg.startswith("i18n") and arg.endswith(".ts")): print(f"Skipped: {item}")
continue continue
file = os.path.basename(arg) if item.is_file():
if not file.startswith("nw_") and len(file) > 6: translations.append(item)
print("Skipping non-novelWriter TS file %s" % file) print(f"Added: {item}")
continue elif item.exists():
continue
if os.path.isfile(arg): else: # Create an empty new language file
translations.append(arg) langCode = item.name[3:-3]
elif os.path.exists(arg): item.write_text(
pass "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
else: # Create an empty new language file "<!DOCTYPE TS>\n"
langCode = file[3:-3] f"<TS version=\"2.0\" language=\"{langCode}\" sourcelanguage=\"en_GB\"/>\n"
writeFile(arg, ( )
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" translations.append(item)
"<!DOCTYPE TS>\n" print(f"Created: {item}")
f"<TS version=\"2.0\" language=\"{langCode}\" sourcelanguage=\"en_GB\"/>\n"
))
translations.append(arg)
else:
print("No translation files selected for update ...")
print("")
return
for translation in translations:
print(translation)
print("") print("")
print("Updating Language Files:") print("Updating Language Files:")
print("") print("")
# Using the pylupdate tool from PyQt6 as it supports TS file format 2.1. lupdate(
lupdate(sources, translations, no_obsolete=True, no_summary=False) sources=[str(f) for f in sources],
translation_files=[str(f) for f in translations],
no_obsolete=True,
no_summary=False,
)
print("") print("")
return return
##
# Qt Linguist QM Builder (qtlrelease)
##
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 = CURR_DIR / "i18n"
dstDir = CURR_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("Qt5 Linguist tools seem to be missing")
print("On Debian/Ubuntu, install: qttools5-dev-tools pyqt5-dev-tools")
print(str(exc))
sys.exit(1)
print("")
print("Moving QM Files to Assets")
print("")
dstRel = dstDir.relative_to(CURR_DIR)
for item in srcDir.iterdir():
if item.is_file() and item.suffix == ".qm":
item.rename(dstDir / item.name)
print("Moved: %s -> %s" % (item.relative_to(CURR_DIR), dstRel / item.name))
print("")
return
##
# Clean Assets (clean-assets)
##
def cleanBuiltAssets(args: argparse.Namespace | None = None) -> None:
"""Remove assets built by this script."""
print("")
print("Removing Built Assets")
print("=====================")
print("")
assets = [
CURR_DIR / "novelwriter" / "assets" / "sample.zip",
CURR_DIR / "novelwriter" / "assets" / "manual.pdf",
]
assets.extend((CURR_DIR / "novelwriter" / "assets" / "i18n").glob("*.qm"))
for asset in assets:
if asset.is_file():
asset.unlink()
print(f"Deleted: {asset.relative_to(CURR_DIR)}")
print("")
return
##
# Build Assets (build-assets)
##
def buildAllAssets(args: argparse.Namespace) -> None:
"""Build all assets."""
cleanBuiltAssets()
buildPdfManual()
buildSampleZip()
buildTranslationAssets()
return
## ##
# Generate MacOS PList # Generate MacOS PList
## ##
@@ -414,76 +484,6 @@ def genMacOSPlist() -> None:
return return
##
# Sample Project ZIP File Builder (sample)
##
def buildSampleZip() -> 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 = "sample"
dstSample = os.path.join("novelwriter", "assets", "sample.zip")
if os.path.isdir(srcSample):
if os.path.isfile(dstSample):
os.unlink(dstSample)
from zipfile import ZipFile
with ZipFile(dstSample, "w") as zipObj:
print("Compressing: nwProject.nwx")
zipObj.write(os.path.join(srcSample, "nwProject.nwx"), "nwProject.nwx")
for docFile in os.listdir(os.path.join(srcSample, "content")):
print("Compressing: content/%s" % docFile)
srcDoc = os.path.join(srcSample, "content", docFile)
zipObj.write(srcDoc, "content/"+docFile)
else:
print("Error: Could not find sample project source directory.")
sys.exit(1)
print("")
print("Built file: %s" % dstSample)
print("")
return
def cleanBuiltAssets() -> None:
"""Remove assets built by this script."""
print("")
print("Removing Built Assets")
print("=====================")
print("")
sampleZip = os.path.join("novelwriter", "assets", "sample.zip")
if os.path.isfile(sampleZip):
print(f"Deleted: {sampleZip}")
os.unlink(sampleZip)
pdfManual = os.path.join("novelwriter", "assets", "manual.pdf")
if os.path.isfile(pdfManual):
print(f"Deleted: {pdfManual}")
os.unlink(pdfManual)
i18nAssets = os.path.join("novelwriter", "assets", "i18n")
for i18nItem in os.listdir(i18nAssets):
i18nPath = os.path.join(i18nAssets, i18nItem)
if os.path.isfile(i18nPath) and i18nPath.endswith(".qm"):
print(f"Deleted: {i18nPath}")
os.unlink(i18nPath)
print("")
return
def checkAssetsExist() -> bool: def checkAssetsExist() -> bool:
"""Check that the necessary compiled assets exist ahead of a build. """Check that the necessary compiled assets exist ahead of a build.
""" """
@@ -1533,6 +1533,10 @@ def xdgUninstall() -> None:
if __name__ == "__main__": if __name__ == "__main__":
"""Parse command line options and run the commands.""" """Parse command line options and run the commands."""
# Detect OS # Detect OS
isLinux = sys.platform.startswith("linux")
isMacOS = sys.platform.startswith("darwin")
isWin = sys.platform.startswith("win32")
if sys.platform.startswith("linux"): if sys.platform.startswith("linux"):
hostOS = OS_LINUX hostOS = OS_LINUX
elif sys.platform.startswith("darwin"): elif sys.platform.startswith("darwin"):
@@ -1546,6 +1550,9 @@ if __name__ == "__main__":
sysArgs = sys.argv.copy() sysArgs = sys.argv.copy()
parser = argparse.ArgumentParser()
parsers = parser.add_subparsers()
# Sign package # Sign package
if "--sign" in sysArgs: if "--sign" in sysArgs:
sysArgs.remove("--sign") sysArgs.remove("--sign")
@@ -1610,115 +1617,142 @@ if __name__ == "__main__":
"", "",
] ]
# Version
cmdVersion = parsers.add_parser(
"version", help="Print the novelWriter version."
)
cmdVersion.set_defaults(func=printVersion)
# General # General
# ======= # =======
if "help" in sysArgs: # Pip Install
sysArgs.remove("help") cmdPipInstall = parsers.add_parser(
print("\n".join(helpMsg)) "pip", help="Install all package dependencies for novelWriter using pip."
sys.exit(0) )
cmdPipInstall.add_argument("--linux", action="store_true", help="For Linux.", default=isLinux)
cmdPipInstall.add_argument("--mac", action="store_true", help="For MacOS.", default=isMacOS)
cmdPipInstall.add_argument("--win", action="store_true", help="For Windows.", default=isWin)
cmdPipInstall.set_defaults(func=installPackages)
if "version" in sysArgs: # Build Clean
sysArgs.remove("version") cmdBuildClean = parsers.add_parser(
print(extractVersion(beQuiet=True)[0], end=None) "build-clean", help="Recursively delete all build folders."
sys.exit(0) )
cmdBuildClean.set_defaults(func=cleanBuildDirs)
if "pip" in sysArgs:
sysArgs.remove("pip")
installPackages(hostOS)
if "build-clean" in sysArgs:
sysArgs.remove("build-clean")
cleanBuildDirs()
# Additional Builds # Additional Builds
# ================= # =================
if "manual" in sysArgs: # Build Manual
sysArgs.remove("manual") cmdBuildManual = parsers.add_parser(
buildPdfManual() "manual", help="Build the help documentation as a PDF (requires LaTeX)."
)
cmdBuildManual.set_defaults(func=buildPdfManual)
if "qtlrelease" in sysArgs: # Build Sample
sysArgs.remove("qtlrelease") cmdBuildSample = parsers.add_parser(
buildQtI18n() "sample", help="Build the sample project zip file and add it to assets."
)
cmdBuildSample.set_defaults(func=buildSampleZip)
if "qtlupdate" in sysArgs: # Update i18n Sources
sysArgs.remove("qtlupdate") cmdUpdateTS = parsers.add_parser(
buildQtI18nTS(sysArgs) "qtlupdate", help=(
sys.exit(0) # Don't continue execution "Update translation files for internationalisation. "
"The files to be updated must be provided as arguments. "
"New files can be created by giving a 'nw_<lang>.ts' file name "
"where <lang> is a valid language code."
)
)
cmdUpdateTS.add_argument("files", nargs="+")
cmdUpdateTS.set_defaults(func=updateTranslationSources)
if "sample" in sysArgs: # Build i18n Files
sysArgs.remove("sample") cmdBuildQM = parsers.add_parser(
buildSampleZip() "qtlrelease", help="Build the language files for internationalisation."
)
cmdBuildQM.set_defaults(func=buildTranslationAssets)
if "clean-assets" in sysArgs: # Clean Assets
sysArgs.remove("clean-assets") cmdCleanAssets = parsers.add_parser(
cleanBuiltAssets() "clean-assets", help="Delete assets built by manual, sample and qtlrelease."
)
cmdCleanAssets.set_defaults(func=cleanBuiltAssets)
if "gen-plist" in sysArgs: # Build Assets
sysArgs.remove("gen-plist") cmdBuildAssets = parsers.add_parser(
genMacOSPlist() "build-assets", help="Build all assets. Includes manual, sample and qtlrelease."
)
cmdBuildAssets.set_defaults(func=buildAllAssets)
# Python Packaging # if "gen-plist" in sysArgs:
# ================ # sysArgs.remove("gen-plist")
# genMacOSPlist()
if "import-i18n" in sysArgs: # # Python Packaging
sysArgs.remove("import-i18n") # # ================
importI18nUpdates(sysArgs)
sys.exit(0) # Don't continue execution
if "windows-zip" in sysArgs: # if "import-i18n" in sysArgs:
sysArgs.remove("windows-zip") # sysArgs.remove("import-i18n")
makeWindowsZip() # importI18nUpdates(sysArgs)
# sys.exit(0) # Don't continue execution
if "build-deb" in sysArgs: # if "windows-zip" in sysArgs:
sysArgs.remove("build-deb") # sysArgs.remove("windows-zip")
if hostOS == OS_LINUX: # makeWindowsZip()
if doSign:
signKey = "D6A9F6B8F227CF7C6F6D1EE84DBBE4B734B0BD08"
else:
signKey = None
makeDebianPackage(signKey=signKey)
else:
print("ERROR: Command 'build-deb' can only be used on Linux")
sys.exit(1)
if "build-ubuntu" in sysArgs: # if "build-deb" in sysArgs:
sysArgs.remove("build-ubuntu") # sysArgs.remove("build-deb")
if hostOS == OS_LINUX: # if hostOS == OS_LINUX:
makeForLaunchpad(doSign=doSign, isFirst=isFirstBuild) # if doSign:
else: # signKey = "D6A9F6B8F227CF7C6F6D1EE84DBBE4B734B0BD08"
print("ERROR: Command 'build-ubuntu' can only be used on Linux") # else:
sys.exit(1) # signKey = None
# makeDebianPackage(signKey=signKey)
# else:
# print("ERROR: Command 'build-deb' can only be used on Linux")
# sys.exit(1)
if "build-win-exe" in sysArgs: # if "build-ubuntu" in sysArgs:
sysArgs.remove("build-win-exe") # sysArgs.remove("build-ubuntu")
makeWindowsEmbedded(sysArgs) # if hostOS == OS_LINUX:
sys.exit(0) # Don't continue execution # makeForLaunchpad(doSign=doSign, isFirst=isFirstBuild)
# else:
# print("ERROR: Command 'build-ubuntu' can only be used on Linux")
# sys.exit(1)
if "build-appimage" in sysArgs: # if "build-win-exe" in sysArgs:
sysArgs.remove("build-appimage") # sysArgs.remove("build-win-exe")
if hostOS == OS_LINUX: # makeWindowsEmbedded(sysArgs)
sysArgs = makeAppImage(sysArgs) # sys.exit(0) # Don't continue execution
else:
print("ERROR: Command 'build-appimage' can only be used on Linux")
sys.exit(1)
# General Installers # if "build-appimage" in sysArgs:
# ================== # sysArgs.remove("build-appimage")
# if hostOS == OS_LINUX:
# sysArgs = makeAppImage(sysArgs)
# else:
# print("ERROR: Command 'build-appimage' can only be used on Linux")
# sys.exit(1)
if "xdg-install" in sysArgs: # # General Installers
sysArgs.remove("xdg-install") # # ==================
if hostOS == OS_WIN:
print("ERROR: Command 'xdg-install' cannot be used on Windows")
sys.exit(1)
else:
xdgInstall()
if "xdg-uninstall" in sysArgs: # if "xdg-install" in sysArgs:
sysArgs.remove("xdg-uninstall") # sysArgs.remove("xdg-install")
if hostOS == OS_WIN: # if hostOS == OS_WIN:
print("ERROR: Command 'xdg-uninstall' cannot be used on Windows") # print("ERROR: Command 'xdg-install' cannot be used on Windows")
sys.exit(1) # sys.exit(1)
else: # else:
xdgUninstall() # xdgInstall()
# if "xdg-uninstall" in sysArgs:
# sysArgs.remove("xdg-uninstall")
# if hostOS == OS_WIN:
# print("ERROR: Command 'xdg-uninstall' cannot be used on Windows")
# sys.exit(1)
# else:
# xdgUninstall()
args = parser.parse_args()
args.func(args)
+1 -1
View File
@@ -18,7 +18,7 @@ if [ ! -d $ENVPATH ]; then
fi fi
source $ENVPATH/bin/activate source $ENVPATH/bin/activate
pip3 install -r docs/source/requirements.txt pip3 install -r docs/source/requirements.txt
python3 pkgutils.py qtlrelease manual sample python3 pkgutils.py build-assets
deactivate deactivate
echo "" echo ""
+1 -2
View File
@@ -17,8 +17,7 @@ if [ ! -d $ENVPATH ]; then
fi fi
source $ENVPATH/bin/activate source $ENVPATH/bin/activate
pip3 install -r docs/source/requirements.txt pip3 install -r docs/source/requirements.txt
python3 pkgutils.py clean-assets python3 pkgutils.py build-assets
python3 pkgutils.py qtlrelease manual sample
deactivate deactivate
echo "" echo ""