From 1b8867537ed489c9eed45d9d915ca53d4569af68 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 4 Jul 2024 18:34:10 +0200 Subject: [PATCH 01/11] Update all assets commands to pkgutils --- .github/workflows/build_linux.yml | 2 +- pkgutils.py | 566 ++++++++++++++++-------------- setup/make_pip.sh | 2 +- setup/make_release.sh | 3 +- 4 files changed, 303 insertions(+), 270 deletions(-) diff --git a/.github/workflows/build_linux.yml b/.github/workflows/build_linux.yml index e6c8705a..9c607318 100644 --- a/.github/workflows/build_linux.yml +++ b/.github/workflows/build_linux.yml @@ -6,7 +6,7 @@ jobs: buildAssets: uses: ./.github/workflows/build_assets.yml - buildLinux: + buildLinux-AppImage: needs: buildAssets runs-on: ubuntu-latest env: diff --git a/pkgutils.py b/pkgutils.py index 657e41e1..45521f27 100755 --- a/pkgutils.py +++ b/pkgutils.py @@ -25,6 +25,7 @@ along with this program. If not, see . """ from __future__ import annotations +import argparse import datetime import email.utils import os @@ -40,6 +41,8 @@ OS_LINUX = 1 OS_WIN = 2 OS_DARWIN = 3 +CURR_DIR = Path(__file__).parent + # =============================================================================================== # # Utilities @@ -131,11 +134,21 @@ def makeCheckSum(sumFile: str, cwd: str | None = None) -> str: # 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) ## -def installPackages(hostOS: int) -> None: +def installPackages(args: argparse.Namespace) -> None: """Install package dependencies both for this script and for running novelWriter itself. """ @@ -145,9 +158,9 @@ def installPackages(hostOS: int) -> None: print("") installQueue = ["pip", "-r requirements.txt"] - if hostOS == OS_DARWIN: + if args.mac: installQueue.append("pyobjc") - elif hostOS == OS_WIN: + elif args.win: installQueue.append("pywin32") pyCmd = [sys.executable, "-m"] @@ -168,28 +181,30 @@ def installPackages(hostOS: int) -> None: # Clean Build and Dist Folders (build-clean) ## -def cleanBuildDirs() -> None: +def cleanBuildDirs(args: argparse.Namespace) -> None: """Recursively delete the 'build' and 'dist' folders.""" print("") print("Cleaning up build environment ...") print("") - def removeFolder(rmDir: str) -> None: - if os.path.isdir(rmDir): - try: - shutil.rmtree(rmDir) - print("Deleted: %s" % rmDir) - except OSError: - print("Failed: %s" % rmDir) - else: - print("Missing: %s" % rmDir) + folders = [ + CURR_DIR / "build", + CURR_DIR / "dist", + CURR_DIR / "dist_deb", + CURR_DIR / "dist_minimal", + CURR_DIR / "dist_appimage", + CURR_DIR / "novelWriter.egg-info", + ] - removeFolder("build") - removeFolder("dist") - removeFolder("dist_deb") - removeFolder("dist_minimal") - removeFolder("dist_appimage") - removeFolder("novelWriter.egg-info") + for folder in folders: + if folder.is_dir(): + try: + shutil.rmtree(folder) + print("Deleted: %s" % folder) + except OSError: + print("Failed: %s" % folder) + else: + print("Missing: %s" % folder) print("") @@ -204,28 +219,23 @@ def cleanBuildDirs() -> None: # Build PDF Manual (manual) ## -def buildPdfManual() -> None: +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 = os.path.join("docs", "build", "latex", "manual.pdf") - finalFile = os.path.join("novelwriter", "assets", "manual.pdf") - - if os.path.isfile(finalFile): - # Make sure a new file is always generated - os.unlink(finalFile) + buildFile = CURR_DIR / "docs" / "build" / "latex" / "manual.pdf" + finalFile = CURR_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: - if os.path.isfile(finalFile): - os.unlink(finalFile) print("") - os.rename(buildFile, finalFile) + buildFile.rename(finalFile) else: 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: - """Build the lang.qm files for Qt Linguist.""" +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 Qt Localisation Files") - print("==============================") - - print("") - print("TS Files to Build:") + print("Building Sample ZIP File") + print("========================") print("") - tsList = [] - for aFile in os.listdir("i18n"): - 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) + srcSample = CURR_DIR / "sample" + dstSample = CURR_DIR / "novelwriter" / "assets" / "sample.zip" - print("") - print("Building Translation Files:") - print("") + 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}") - try: - subprocess.call(["lrelease", "-verbose", *tsList]) - 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)) + else: + print("Error: Could not find sample project source directory.") sys.exit(1) print("") - print("Moving QM Files to Assets") - 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("Built file: %s" % dstSample) print("") return @@ -311,13 +304,14 @@ def buildQtI18n() -> None: # 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.""" print("") print("Building Qt Translation Files") print("=============================") try: + # Using the pylupdate tool from PyQt6 as it supports TS file format 2.1. from PyQt6.lupdate.lupdate import lupdate except ImportError: print("ERROR: This command requires lupdate from PyQt6") @@ -328,65 +322,141 @@ def buildQtI18nTS(sysArgs: list[str]) -> None: print("Scanning Source Tree:") print("") - sources = [os.path.join("i18n", "qtbase.py")] - for root, _, files in os.walk("novelwriter"): - 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) - + sources = list((CURR_DIR / "novelwriter").glob("**/*.py")) + sources.insert(0, CURR_DIR / "i18n" / "qtbase.py") for source in sources: - print(source) + print(source.relative_to(CURR_DIR)) print("") print("TS Files to Update:") print("") translations = [] - if len(sysArgs) >= 2: - for arg in sysArgs[1:]: - if not (arg.startswith("i18n") and arg.endswith(".ts")): - continue + 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 - file = os.path.basename(arg) - if not file.startswith("nw_") and len(file) > 6: - print("Skipping non-novelWriter TS file %s" % file) - continue - - if os.path.isfile(arg): - translations.append(arg) - elif os.path.exists(arg): - pass - else: # Create an empty new language file - langCode = file[3:-3] - writeFile(arg, ( - "\n" - "\n" - f"\n" - )) - translations.append(arg) - - else: - print("No translation files selected for update ...") - print("") - return - - for translation in translations: - print(translation) + 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] + item.write_text( + "\n" + "\n" + f"\n" + ) + translations.append(item) + print(f"Created: {item}") print("") print("Updating Language Files:") print("") - # Using the pylupdate tool from PyQt6 as it supports TS file format 2.1. - lupdate(sources, translations, no_obsolete=True, no_summary=False) + lupdate( + sources=[str(f) for f in sources], + translation_files=[str(f) for f in translations], + no_obsolete=True, + no_summary=False, + ) print("") 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 ## @@ -414,76 +484,6 @@ def genMacOSPlist() -> None: 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: """Check that the necessary compiled assets exist ahead of a build. """ @@ -1533,6 +1533,10 @@ def xdgUninstall() -> None: if __name__ == "__main__": """Parse command line options and run the commands.""" # Detect OS + isLinux = sys.platform.startswith("linux") + isMacOS = sys.platform.startswith("darwin") + isWin = sys.platform.startswith("win32") + if sys.platform.startswith("linux"): hostOS = OS_LINUX elif sys.platform.startswith("darwin"): @@ -1546,6 +1550,9 @@ if __name__ == "__main__": sysArgs = sys.argv.copy() + parser = argparse.ArgumentParser() + parsers = parser.add_subparsers() + # Sign package if "--sign" in sysArgs: 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 # ======= - if "help" in sysArgs: - sysArgs.remove("help") - print("\n".join(helpMsg)) - sys.exit(0) + # Pip Install + cmdPipInstall = parsers.add_parser( + "pip", help="Install all package dependencies for novelWriter using pip." + ) + 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: - sysArgs.remove("version") - print(extractVersion(beQuiet=True)[0], end=None) - sys.exit(0) - - if "pip" in sysArgs: - sysArgs.remove("pip") - installPackages(hostOS) - - if "build-clean" in sysArgs: - sysArgs.remove("build-clean") - cleanBuildDirs() + # Build Clean + cmdBuildClean = parsers.add_parser( + "build-clean", help="Recursively delete all build folders." + ) + cmdBuildClean.set_defaults(func=cleanBuildDirs) # Additional Builds # ================= - if "manual" in sysArgs: - sysArgs.remove("manual") - buildPdfManual() + # Build Manual + cmdBuildManual = parsers.add_parser( + "manual", help="Build the help documentation as a PDF (requires LaTeX)." + ) + cmdBuildManual.set_defaults(func=buildPdfManual) - if "qtlrelease" in sysArgs: - sysArgs.remove("qtlrelease") - buildQtI18n() + # Build Sample + cmdBuildSample = parsers.add_parser( + "sample", help="Build the sample project zip file and add it to assets." + ) + cmdBuildSample.set_defaults(func=buildSampleZip) - if "qtlupdate" in sysArgs: - sysArgs.remove("qtlupdate") - buildQtI18nTS(sysArgs) - sys.exit(0) # Don't continue execution + # Update i18n Sources + cmdUpdateTS = parsers.add_parser( + "qtlupdate", help=( + "Update translation files for internationalisation. " + "The files to be updated must be provided as arguments. " + "New files can be created by giving a 'nw_.ts' file name " + "where is a valid language code." + ) + ) + cmdUpdateTS.add_argument("files", nargs="+") + cmdUpdateTS.set_defaults(func=updateTranslationSources) - if "sample" in sysArgs: - sysArgs.remove("sample") - buildSampleZip() + # Build i18n Files + cmdBuildQM = parsers.add_parser( + "qtlrelease", help="Build the language files for internationalisation." + ) + cmdBuildQM.set_defaults(func=buildTranslationAssets) - if "clean-assets" in sysArgs: - sysArgs.remove("clean-assets") - cleanBuiltAssets() + # Clean Assets + cmdCleanAssets = parsers.add_parser( + "clean-assets", help="Delete assets built by manual, sample and qtlrelease." + ) + cmdCleanAssets.set_defaults(func=cleanBuiltAssets) - if "gen-plist" in sysArgs: - sysArgs.remove("gen-plist") - genMacOSPlist() + # Build Assets + cmdBuildAssets = parsers.add_parser( + "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: - sysArgs.remove("import-i18n") - importI18nUpdates(sysArgs) - sys.exit(0) # Don't continue execution + # # Python Packaging + # # ================ - if "windows-zip" in sysArgs: - sysArgs.remove("windows-zip") - makeWindowsZip() + # if "import-i18n" in sysArgs: + # sysArgs.remove("import-i18n") + # importI18nUpdates(sysArgs) + # sys.exit(0) # Don't continue execution - if "build-deb" in sysArgs: - sysArgs.remove("build-deb") - if hostOS == OS_LINUX: - 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 "windows-zip" in sysArgs: + # sysArgs.remove("windows-zip") + # makeWindowsZip() - if "build-ubuntu" in sysArgs: - sysArgs.remove("build-ubuntu") - if hostOS == OS_LINUX: - makeForLaunchpad(doSign=doSign, isFirst=isFirstBuild) - else: - print("ERROR: Command 'build-ubuntu' can only be used on Linux") - sys.exit(1) + # if "build-deb" in sysArgs: + # sysArgs.remove("build-deb") + # if hostOS == OS_LINUX: + # 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-win-exe" in sysArgs: - sysArgs.remove("build-win-exe") - makeWindowsEmbedded(sysArgs) - sys.exit(0) # Don't continue execution + # if "build-ubuntu" in sysArgs: + # sysArgs.remove("build-ubuntu") + # if hostOS == OS_LINUX: + # 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: - 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 "build-win-exe" in sysArgs: + # sysArgs.remove("build-win-exe") + # makeWindowsEmbedded(sysArgs) + # sys.exit(0) # Don't continue execution - # 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: - sysArgs.remove("xdg-install") - if hostOS == OS_WIN: - print("ERROR: Command 'xdg-install' cannot be used on Windows") - sys.exit(1) - else: - xdgInstall() + # # General Installers + # # ================== - 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() + # if "xdg-install" in sysArgs: + # 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: + # 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) diff --git a/setup/make_pip.sh b/setup/make_pip.sh index 7d3817e0..6ddf21bc 100755 --- a/setup/make_pip.sh +++ b/setup/make_pip.sh @@ -18,7 +18,7 @@ if [ ! -d $ENVPATH ]; then fi source $ENVPATH/bin/activate pip3 install -r docs/source/requirements.txt -python3 pkgutils.py qtlrelease manual sample +python3 pkgutils.py build-assets deactivate echo "" diff --git a/setup/make_release.sh b/setup/make_release.sh index 0904000f..c5237745 100755 --- a/setup/make_release.sh +++ b/setup/make_release.sh @@ -17,8 +17,7 @@ if [ ! -d $ENVPATH ]; then fi source $ENVPATH/bin/activate pip3 install -r docs/source/requirements.txt -python3 pkgutils.py clean-assets -python3 pkgutils.py qtlrelease manual sample +python3 pkgutils.py build-assets deactivate echo "" From 7c65d22452ca89c5275b5ad97001f24394224338 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 4 Jul 2024 19:45:54 +0200 Subject: [PATCH 02/11] Updated debian packaging code --- pkgutils.py | 356 ++++++++++++++++++++++++---------------------------- 1 file changed, 166 insertions(+), 190 deletions(-) diff --git a/pkgutils.py b/pkgutils.py index 45521f27..d40f37b5 100755 --- a/pkgutils.py +++ b/pkgutils.py @@ -36,12 +36,12 @@ import zipfile from pathlib import Path -OS_NONE = 0 -OS_LINUX = 1 -OS_WIN = 2 -OS_DARWIN = 3 - CURR_DIR = Path(__file__).parent +SIGN_KEY = "D6A9F6B8F227CF7C6F6D1EE84DBBE4B734B0BD08" + +OS_LINUX = sys.platform.startswith("linux") +OS_DARWIN = sys.platform.startswith("darwin") +OS_WIN = sys.platform.startswith("win32") # =============================================================================================== # @@ -90,6 +90,27 @@ def stripVersion(version: str) -> str: return version +def copySourceCode(dst: Path) -> None: + """Copy the novelwriter source tree to path.""" + src = CURR_DIR / "novelwriter" + for item in src.glob("**/*"): + relSrc = item.relative_to(CURR_DIR) + if item.suffix in (".pyc", ".pyo"): + print(f"Ignore: {relSrc}") + continue + + if item.parent.is_dir() and item.parent.name != "__pycache__": + dstDir = dst / relSrc.parent + if not dstDir.exists(): + dstDir.mkdir(parents=True) + print(f"Folder: {dstDir}") + + if item.is_file(): + shutil.copyfile(item, dst / relSrc) + print(f"Copied: {dst / relSrc}") + return + + def readFile(fileName: str) -> str: """Read an entire file and return as a string.""" return Path(fileName).read_text(encoding="utf-8") @@ -112,7 +133,7 @@ def toUpload(srcPath: str | Path, dstName: str | None = None) -> None: return -def makeCheckSum(sumFile: str, cwd: str | None = None) -> str: +def makeCheckSum(sumFile: str, cwd: Path | str | None = None) -> str: """Create a SHA256 checksum file.""" try: if cwd is None: @@ -130,6 +151,32 @@ def makeCheckSum(sumFile: str, cwd: str | None = None) -> str: return shaFile +def checkAssetsExist() -> bool: + """Check that the necessary assets exist ahead of a build.""" + hasSample = False + hasManual = False + hasQmData = False + + sampleZip = os.path.join("novelwriter", "assets", "sample.zip") + if os.path.isfile(sampleZip): + print(f"Found: {sampleZip}") + hasSample = True + + pdfManual = os.path.join("novelwriter", "assets", "manual.pdf") + if os.path.isfile(pdfManual): + print(f"Found: {pdfManual}") + hasManual = True + + 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"Found: {i18nPath}") + hasQmData = True + + return hasSample and hasManual and hasQmData + + # =============================================================================================== # # General # =============================================================================================== # @@ -484,33 +531,6 @@ def genMacOSPlist() -> None: return -def checkAssetsExist() -> bool: - """Check that the necessary compiled assets exist ahead of a build. - """ - hasSample = False - hasManual = False - hasQmData = False - - sampleZip = os.path.join("novelwriter", "assets", "sample.zip") - if os.path.isfile(sampleZip): - print(f"Found: {sampleZip}") - hasSample = True - - pdfManual = os.path.join("novelwriter", "assets", "manual.pdf") - if os.path.isfile(pdfManual): - print(f"Found: {pdfManual}") - hasManual = True - - 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"Found: {i18nPath}") - hasQmData = True - - return hasSample and hasManual and hasQmData - - # =============================================================================================== # # Python Packaging # =============================================================================================== # @@ -519,36 +539,32 @@ def checkAssetsExist() -> bool: # Import Translations (import-i18n) ## -def importI18nUpdates(sysArgs: list[str]) -> None: +def importI18nUpdates(args: argparse.Namespace) -> None: """Import new translation files from a zip file.""" print("") print("Import Updated Translations") print("===========================") print("") - fileName = None - if len(sysArgs) >= 2: - if os.path.isfile(sysArgs[1]): - fileName = sysArgs[1] - - if fileName is None: + fileName = Path(args.file).absolute() + if not fileName.is_file(): print("File not found ...") sys.exit(1) - projPath = os.path.join("novelwriter", "assets", "i18n") - mainPath = "i18n" + dstPath = CURR_DIR / "novelwriter" / "assets" / "i18n" + srcPath = CURR_DIR / "i18n" - print("Loading file: %s" % fileName) + print(f"Loading file: {fileName}") with zipfile.ZipFile(fileName) as zipObj: - for archFile in zipObj.namelist(): - if archFile.startswith("nw_") and archFile.endswith(".ts"): - zipObj.extract(archFile, mainPath) - print("Extracted: %s > %s" % (archFile, os.path.join(mainPath, archFile))) - elif archFile.startswith("project_") and archFile.endswith(".json"): - zipObj.extract(archFile, projPath) - print("Extracted: %s > %s" % (archFile, os.path.join(projPath, archFile))) + 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("Skipped: %s" % archFile) + print(f"Skipped: {item}") print("") @@ -628,7 +644,7 @@ def makeWindowsZip() -> None: ## -# Make Debian Package (build-deb) +# Make Debian Package ## def makeDebianPackage( @@ -660,21 +676,20 @@ def makeDebianPackage( # Set Up Folder # ============= - bldDir = "dist_deb" + refDir = CURR_DIR / "setup" + bldDir = CURR_DIR / "dist_deb" bldPkg = f"novelwriter_{pkgVers}" - outDir = f"{bldDir}/{bldPkg}" - debDir = f"{outDir}/debian" - datDir = f"{outDir}/data" + outDir = bldDir / bldPkg + debDir = outDir / "debian" + datDir = outDir / "data" - if not os.path.isdir(bldDir): - os.mkdir(bldDir) - - if os.path.isdir(outDir): + bldDir.mkdir(exist_ok=True) + if outDir.exists(): print("Removing old build files ...") print("") shutil.rmtree(outDir) - os.mkdir(outDir) + outDir.mkdir(exist_ok=False) # Check Additional Assets # ======================= @@ -688,29 +703,7 @@ def makeDebianPackage( print("Copying novelWriter source ...") print("") - - for nPath, _, nFiles in os.walk("novelwriter"): - if nPath.endswith("__pycache__"): - print("Skipped: %s" % nPath) - continue - - pPath = f"{outDir}/{nPath}" - if not os.path.isdir(pPath): - os.mkdir(pPath) - - fCount = 0 - for fFile in nFiles: - nFile = f"{nPath}/{fFile}" - pFile = f"{pPath}/{fFile}" - - if fFile.endswith(".pyc"): - print("Skipped: %s" % nFile) - continue - - shutil.copyfile(nFile, pFile) - fCount += 1 - - print("Copied: %s/* [Files: %d]" % (nPath, fCount)) + copySourceCode(outDir) print("") print("Copying or generating additional files ...") @@ -724,63 +717,60 @@ def makeDebianPackage( shutil.copyfile(copyFile, f"{outDir}/{copyFile}") print("Copied: %s" % copyFile) - writeFile(f"{outDir}/MANIFEST.in", ( + (outDir / "MANIFEST.in").write_text( "include LICENSE.md\n" "include CREDITS.md\n" "include CHANGELOG.md\n" "include data/*\n" "recursive-include novelwriter/assets *\n" - )) + ) print("Wrote: MANIFEST.in") - writeFile(f"{outDir}/setup.py", ( + (outDir / "setup.py").write_text( "import setuptools\n" "setuptools.setup()\n" - )) + ) print("Wrote: setup.py") if oldSetuptools: # This is needed for Ubuntu up to 22.04 - setupCfg = readFile("setup/launchpad_setup.cfg").replace( - "file: setup/description_pypi.md", "file: data/description_short.txt" - ) - writeFile(f"{outDir}/setup.cfg", setupCfg) + text = (CURR_DIR / "setup" / "launchpad_setup.cfg").read_text() + text.replace("setup/description_pypi.md", "data/description_short.txt") + (outDir / "setup.cfg").write_text(text) print("Wrote: setup.cfg") - writeFile(f"{outDir}/pyproject.toml", ( + (outDir / "pyproject.toml").write_text( "[build-system]\n" "requires = [\"setuptools\"]\n" "build-backend = \"setuptools.build_meta\"\n" - )) - print("Wrote: pyproject.toml") - - else: - pyProject = readFile("pyproject.toml").replace( - "setup/description_pypi.md", "data/description_short.txt" ) - writeFile(f"{outDir}/pyproject.toml", pyProject) + print("Wrote: pyproject.toml") + else: + text = (CURR_DIR / "pyproject.toml").read_text() + text.replace("setup/description_pypi.md", "data/description_short.txt") + (outDir / "pyproject.toml").write_text(text) print("Wrote: pyproject.toml") # Copy/Write Debian Files # ======================= - shutil.copytree("setup/debian", debDir) + shutil.copytree(refDir / "debian", debDir) print("Copied: debian/*") - writeFile(f"{debDir}/changelog", ( + (debDir / "changelog").write_text( f"novelwriter ({pkgVers}) {distName}; urgency=low\n\n" f" * Update to version {pkgVers}\n\n" f" -- Veronica Berglyd Olsen {pkgDate}\n" - )) + ) print("Wrote: debian/changelog") # Copy/Write Data Files # ===================== - shutil.copytree("setup/data", datDir) + shutil.copytree(refDir / "data", datDir) print("Copied: data/*") - shutil.copyfile("setup/description_short.txt", f"{outDir}/data/description_short.txt") + shutil.copyfile(refDir / "description_short.txt", outDir / "data" / "description_short.txt") print("Copied: data/description_short.txt") # Build Package @@ -797,13 +787,12 @@ def makeDebianPackage( if sourceBuild: subprocess.call(["debuild", "-S"] + signArgs, cwd=outDir) - toUpload(f"{bldDir}/{bldPkg}.tar.xz") + toUpload(bldDir / f"{bldPkg}.tar.xz") else: subprocess.call(["dpkg-buildpackage"] + signArgs, cwd=outDir) - shutil.copyfile(f"{bldDir}/{bldPkg}.tar.xz", f"{bldDir}/{bldPkg}.debian.tar.xz") - toUpload(f"{bldDir}/{bldPkg}.debian.tar.xz") - toUpload(f"{bldDir}/{bldPkg}_all.deb") - + 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)) @@ -819,17 +808,35 @@ def makeDebianPackage( ## -# Make Launchpad Package (build-ubuntu) +# Build Debian Package (build-deb) ## -def makeForLaunchpad(doSign: bool = False, isFirst: bool = False) -> None: +def buildDebianPackage(args: argparse.Namespace) -> None: + """Build a .deb package""" + if not OS_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 + + +## +# Build Launchpad Packages (build-ubuntu) +## + +def buildForLaunchpad(args: argparse.Namespace) -> None: """Wrapper for building Debian packages for Launchpad.""" + if not OS_LINUX: + print("ERROR: Command 'build-ubuntu' can only be used on Linux") + sys.exit(1) + print("") print("Launchpad Packages") print("==================") print("") - if isFirst: + if args.first: bldNum = "0" else: bldNum = input("Build number [0]: ") @@ -848,10 +855,7 @@ def makeForLaunchpad(doSign: bool = False, isFirst: bool = False) -> None: print(f" * Ubuntu {distNum} {codeName.title()}") print("") - if doSign: - signKey = "D6A9F6B8F227CF7C6F6D1EE84DBBE4B734B0BD08" - else: - signKey = None + signKey = SIGN_KEY if args.sign else None print(f"Sign Key: {str(signKey)}") print("") @@ -1532,41 +1536,9 @@ def xdgUninstall() -> None: if __name__ == "__main__": """Parse command line options and run the commands.""" - # Detect OS - isLinux = sys.platform.startswith("linux") - isMacOS = sys.platform.startswith("darwin") - isWin = sys.platform.startswith("win32") - - if sys.platform.startswith("linux"): - hostOS = OS_LINUX - elif sys.platform.startswith("darwin"): - hostOS = OS_DARWIN - elif sys.platform.startswith("win32"): - hostOS = OS_WIN - elif sys.platform.startswith("cygwin"): - hostOS = OS_WIN - else: - hostOS = OS_NONE - - sysArgs = sys.argv.copy() - parser = argparse.ArgumentParser() parsers = parser.add_subparsers() - # Sign package - if "--sign" in sysArgs: - sysArgs.remove("--sign") - doSign = True - else: - doSign = False - - # First build - if "--first" in sysArgs: - sysArgs.remove("--first") - isFirstBuild = True - else: - isFirstBuild = False - helpMsg = [ "", "novelWriter Setup Tool", @@ -1630,9 +1602,9 @@ if __name__ == "__main__": cmdPipInstall = parsers.add_parser( "pip", help="Install all package dependencies for novelWriter using pip." ) - 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.add_argument("--linux", action="store_true", help="For Linux.", default=OS_LINUX) + cmdPipInstall.add_argument("--mac", action="store_true", help="For MacOS.", default=OS_DARWIN) + cmdPipInstall.add_argument("--win", action="store_true", help="For Windows.", default=OS_WIN) cmdPipInstall.set_defaults(func=installPackages) # Build Clean @@ -1644,17 +1616,12 @@ if __name__ == "__main__": # Additional Builds # ================= - # Build Manual - cmdBuildManual = parsers.add_parser( - "manual", help="Build the help documentation as a PDF (requires LaTeX)." + # Import Translations + cmdImportTS = parsers.add_parser( + "qtlimport", help="Import updated i18n files from a zip file." ) - cmdBuildManual.set_defaults(func=buildPdfManual) - - # Build Sample - cmdBuildSample = parsers.add_parser( - "sample", help="Build the sample project zip file and add it to assets." - ) - cmdBuildSample.set_defaults(func=buildSampleZip) + cmdImportTS.add_argument("file", help="Path to zip file from Crowdin") + cmdImportTS.set_defaults(func=importI18nUpdates) # Update i18n Sources cmdUpdateTS = parsers.add_parser( @@ -1674,6 +1641,18 @@ if __name__ == "__main__": ) cmdBuildQM.set_defaults(func=buildTranslationAssets) + # Build Manual + cmdBuildManual = parsers.add_parser( + "manual", help="Build the help documentation as a PDF (requires LaTeX)." + ) + cmdBuildManual.set_defaults(func=buildPdfManual) + + # Build Sample + cmdBuildSample = parsers.add_parser( + "sample", help="Build the sample project zip file and add it to assets." + ) + cmdBuildSample.set_defaults(func=buildSampleZip) + # Clean Assets cmdCleanAssets = parsers.add_parser( "clean-assets", help="Delete assets built by manual, sample and qtlrelease." @@ -1686,42 +1665,39 @@ if __name__ == "__main__": ) cmdBuildAssets.set_defaults(func=buildAllAssets) + # Python Packaging + # ================ + + # Build Debian Package + cmdBuildDeb = parsers.add_parser( + "build-deb", help=( + "Build a .deb package for Debian and Ubuntu. " + "Add --sign to sign package." + ) + ) + cmdBuildDeb.add_argument("--sign", action="store_true", help="Sign the package.") + cmdBuildDeb.set_defaults(func=buildDebianPackage) + + # Build Ubuntu Packages + cmdBuildUbuntu = parsers.add_parser( + "build-ubuntu", help=( + "Build a .deb package for Debian and Ubuntu. " + "Add --sign to sign package. " + "Add --first to set build number to 0." + ) + ) + cmdBuildUbuntu.add_argument("--sign", action="store_true", help="Sign the package.") + cmdBuildUbuntu.add_argument("--first", action="store_true", help="Set build number to 0.") + cmdBuildUbuntu.set_defaults(func=buildForLaunchpad) + # if "gen-plist" in sysArgs: # sysArgs.remove("gen-plist") # genMacOSPlist() - # # Python Packaging - # # ================ - - # if "import-i18n" in sysArgs: - # sysArgs.remove("import-i18n") - # importI18nUpdates(sysArgs) - # sys.exit(0) # Don't continue execution - # if "windows-zip" in sysArgs: # sysArgs.remove("windows-zip") # makeWindowsZip() - # if "build-deb" in sysArgs: - # sysArgs.remove("build-deb") - # if hostOS == OS_LINUX: - # 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: - # sysArgs.remove("build-ubuntu") - # if hostOS == OS_LINUX: - # makeForLaunchpad(doSign=doSign, isFirst=isFirstBuild) - # else: - # print("ERROR: Command 'build-ubuntu' can only be used on Linux") - # sys.exit(1) - # if "build-win-exe" in sysArgs: # sysArgs.remove("build-win-exe") # makeWindowsEmbedded(sysArgs) From 4d716555af8e545de9990abb462bed26768633a0 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 4 Jul 2024 20:31:53 +0200 Subject: [PATCH 03/11] Updated AppImage packaging code --- pkgutils.py | 352 +++++++++++++++++++++++----------------------------- 1 file changed, 153 insertions(+), 199 deletions(-) diff --git a/pkgutils.py b/pkgutils.py index d40f37b5..7735dca4 100755 --- a/pkgutils.py +++ b/pkgutils.py @@ -37,6 +37,7 @@ import zipfile from pathlib import Path CURR_DIR = Path(__file__).parent +SETUP_DIR = CURR_DIR / "setup" SIGN_KEY = "D6A9F6B8F227CF7C6F6D1EE84DBBE4B734B0BD08" OS_LINUX = sys.platform.startswith("linux") @@ -90,27 +91,6 @@ def stripVersion(version: str) -> str: return version -def copySourceCode(dst: Path) -> None: - """Copy the novelwriter source tree to path.""" - src = CURR_DIR / "novelwriter" - for item in src.glob("**/*"): - relSrc = item.relative_to(CURR_DIR) - if item.suffix in (".pyc", ".pyo"): - print(f"Ignore: {relSrc}") - continue - - if item.parent.is_dir() and item.parent.name != "__pycache__": - dstDir = dst / relSrc.parent - if not dstDir.exists(): - dstDir.mkdir(parents=True) - print(f"Folder: {dstDir}") - - if item.is_file(): - shutil.copyfile(item, dst / relSrc) - print(f"Copied: {dst / relSrc}") - return - - def readFile(fileName: str) -> str: """Read an entire file and return as a string.""" return Path(fileName).read_text(encoding="utf-8") @@ -347,6 +327,42 @@ def buildSampleZip(args: argparse.Namespace | None = None) -> None: return +## +# Import Translations (import-i18n) +## + +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 = CURR_DIR / "novelwriter" / "assets" / "i18n" + srcPath = CURR_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 + + ## # Qt Linguist TS Builder (qtlupdate) ## @@ -536,37 +552,66 @@ def genMacOSPlist() -> None: # =============================================================================================== # ## -# Import Translations (import-i18n) +# Copy Source ## -def importI18nUpdates(args: argparse.Namespace) -> None: - """Import new translation files from a zip file.""" - print("") - print("Import Updated Translations") - print("===========================") - print("") +def copySourceCode(dst: Path) -> None: + """Copy the novelwriter source tree to path.""" + src = CURR_DIR / "novelwriter" + for item in src.glob("**/*"): + relSrc = item.relative_to(CURR_DIR) + if item.suffix in (".pyc", ".pyo"): + print(f"Ignore: {relSrc}") + continue + if item.parent.is_dir() and item.parent.name != "__pycache__": + dstDir = dst / relSrc.parent + if not dstDir.exists(): + dstDir.mkdir(parents=True) + print(f"Folder: {dstDir}") + if item.is_file(): + shutil.copyfile(item, dst / relSrc) + print(f"Copied: {dst / relSrc}") + return - fileName = Path(args.file).absolute() - if not fileName.is_file(): - print("File not found ...") - sys.exit(1) - dstPath = CURR_DIR / "novelwriter" / "assets" / "i18n" - srcPath = CURR_DIR / "i18n" +## +# Copy Package Files +## - 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}") +def copyPackageFiles(dst: Path, useCfg: bool = False) -> None: + """Copy files needed for packaging.""" - print("") + copyFiles = ["LICENSE.md", "CREDITS.md", "pyproject.toml"] + for copyFile in copyFiles: + shutil.copyfile(copyFile, dst / copyFile) + print("Copied: %s" % copyFile) + + (dst / "MANIFEST.in").write_text( + "include LICENSE.md\n" + "include CREDITS.md\n" + # "include data/*\n" + "recursive-include novelwriter/assets *\n" + ) + print("Wrote: MANIFEST.in") + + if useCfg: + # This is needed for Ubuntu up to 22.04 + text = (SETUP_DIR / "launchpad_setup.cfg").read_text() + text = text.replace("setup/description_pypi.md", "data/description_short.txt") + (dst / "setup.cfg").write_text(text) + print("Wrote: setup.cfg") + + (dst / "pyproject.toml").write_text( + "[build-system]\n" + "requires = [\"setuptools\"]\n" + "build-backend = \"setuptools.build_meta\"\n" + ) + print("Wrote: pyproject.toml") + else: + text = (CURR_DIR / "pyproject.toml").read_text() + text = text.replace("setup/description_pypi.md", "data/description_short.txt") + (dst / "pyproject.toml").write_text(text) + print("Wrote: pyproject.toml") return @@ -676,7 +721,6 @@ def makeDebianPackage( # Set Up Folder # ============= - refDir = CURR_DIR / "setup" bldDir = CURR_DIR / "dist_deb" bldPkg = f"novelwriter_{pkgVers}" outDir = bldDir / bldPkg @@ -703,58 +747,19 @@ def makeDebianPackage( print("Copying novelWriter source ...") print("") + copySourceCode(outDir) print("") print("Copying or generating additional files ...") print("") - # Copy/Write Root Files - # ===================== - - copyFiles = ["LICENSE.md", "CREDITS.md", "CHANGELOG.md", "pyproject.toml"] - for copyFile in copyFiles: - shutil.copyfile(copyFile, f"{outDir}/{copyFile}") - print("Copied: %s" % copyFile) - - (outDir / "MANIFEST.in").write_text( - "include LICENSE.md\n" - "include CREDITS.md\n" - "include CHANGELOG.md\n" - "include data/*\n" - "recursive-include novelwriter/assets *\n" - ) - print("Wrote: MANIFEST.in") - - (outDir / "setup.py").write_text( - "import setuptools\n" - "setuptools.setup()\n" - ) - print("Wrote: setup.py") - - if oldSetuptools: - # This is needed for Ubuntu up to 22.04 - text = (CURR_DIR / "setup" / "launchpad_setup.cfg").read_text() - text.replace("setup/description_pypi.md", "data/description_short.txt") - (outDir / "setup.cfg").write_text(text) - print("Wrote: setup.cfg") - - (outDir / "pyproject.toml").write_text( - "[build-system]\n" - "requires = [\"setuptools\"]\n" - "build-backend = \"setuptools.build_meta\"\n" - ) - print("Wrote: pyproject.toml") - else: - text = (CURR_DIR / "pyproject.toml").read_text() - text.replace("setup/description_pypi.md", "data/description_short.txt") - (outDir / "pyproject.toml").write_text(text) - print("Wrote: pyproject.toml") + copyPackageFiles(outDir, oldSetuptools) # Copy/Write Debian Files # ======================= - shutil.copytree(refDir / "debian", debDir) + shutil.copytree(SETUP_DIR / "debian", debDir) print("Copied: debian/*") (debDir / "changelog").write_text( @@ -767,10 +772,10 @@ def makeDebianPackage( # Copy/Write Data Files # ===================== - shutil.copytree(refDir / "data", datDir) + shutil.copytree(SETUP_DIR / "data", datDir) print("Copied: data/*") - shutil.copyfile(refDir / "description_short.txt", outDir / "data" / "description_short.txt") + shutil.copyfile(SETUP_DIR / "description_short.txt", outDir / "data" / "description_short.txt") print("Copied: data/description_short.txt") # Build Package @@ -884,14 +889,11 @@ def buildForLaunchpad(args: argparse.Namespace) -> None: ## -# Make AppImage (build-appimage) +# Build AppImage (build-appimage) ## -def makeAppImage(sysArgs: list[str]) -> list[str]: +def buildAppImage(args: argparse.Namespace) -> None: """Build an AppImage.""" - import argparse - import glob - try: import python_appimage # noqa: F401 # type: ignore except ImportError: @@ -901,32 +903,15 @@ def makeAppImage(sysArgs: list[str]) -> list[str]: ) sys.exit(1) + if not OS_LINUX: + print("ERROR: Command 'build-ubuntu' can only be used on Linux") + sys.exit(1) + print("") print("Build AppImage") print("==============") print("") - parser = argparse.ArgumentParser( - prog="build_appimage", - description="Build an AppImage", - epilog="see https://appimage.org/ for more details", - ) - parser.add_argument( - "--linux-tag", - nargs="?", - default="manylinux_2_28_x86_64", - help=( - "Linux compatibility tag (e.g. manylinux_2_28_x86_64)\n" - "see https://python-appimage.readthedocs.io/en/latest/#available-python-appimages \n" - "and https://github.com/pypa/manylinux for a list of valid tags" - ), - ) - parser.add_argument( - "--python-version", nargs="?", default="3.11", help="Python version (e.g. 3.11)" - ) - - args, unparsedArgs = parser.parse_known_args(sysArgs) - linuxTag = args.linux_tag pythonVer = args.python_version @@ -940,129 +925,77 @@ def makeAppImage(sysArgs: list[str]) -> list[str]: # Set Up Folder # ============= - bldDir = "dist_appimage" + bldDir = CURR_DIR / "dist_appimage" bldPkg = f"novelwriter_{pkgVers}" - outDir = f"{bldDir}/{bldPkg}" - imageDir = f"{bldDir}/appimage" + outDir = bldDir / bldPkg + imgDir = bldDir / "appimage" # Set Up Folders # ============== - if not os.path.isdir(bldDir): - os.mkdir(bldDir) + bldDir.mkdir(exist_ok=True) - if os.path.isdir(outDir): + if outDir.exists(): print("Removing old build files ...") print("") shutil.rmtree(outDir) - os.mkdir(outDir) + outDir.mkdir() - if os.path.isdir(imageDir): + if imgDir.exists(): print("Removing old build metadata files ...") print("") - shutil.rmtree(imageDir) + shutil.rmtree(imgDir) - os.mkdir(imageDir) + imgDir.mkdir() # Remove old AppImages - outFiles = glob.glob(f"{bldDir}/*.AppImage") - if outFiles: + if images := bldDir.glob("*.AppImage"): print("Removing old AppImages") print("") - for image in outFiles: - try: - os.remove(image) - except OSError: - print("Error while deleting file : ", image) + for image in images: + image.unlink() # Copy novelWriter Source # ======================= print("Copying novelWriter source ...") print("") - - for nPath, _, nFiles in os.walk("novelwriter"): - if nPath.endswith("__pycache__"): - print("Skipped: %s" % nPath) - continue - - pPath = f"{outDir}/{nPath}" - if not os.path.isdir(pPath): - os.mkdir(pPath) - - fCount = 0 - for fFile in nFiles: - nFile = f"{nPath}/{fFile}" - pFile = f"{pPath}/{fFile}" - - if fFile.endswith(".pyc"): - print("Skipped: %s" % nFile) - continue - - shutil.copyfile(nFile, pFile) - fCount += 1 - - print("Copied: %s/* [Files: %d]" % (nPath, fCount)) + copySourceCode(outDir) print("") print("Copying or generating additional files ...") print("") - # Copy/Write Root Files - # ===================== - - copyFiles = ["LICENSE.md", "CREDITS.md", "CHANGELOG.md", "pyproject.toml"] - for copyFile in copyFiles: - shutil.copyfile(copyFile, f"{outDir}/{copyFile}") - print("Copied: %s" % copyFile) - - writeFile(f"{outDir}/MANIFEST.in", ( - "include LICENSE.md\n" - "include CREDITS.md\n" - "include CHANGELOG.md\n" - "include data/*\n" - "recursive-include novelwriter/assets *\n" - )) - print("Wrote: MANIFEST.in") - - writeFile(f"{outDir}/setup.py", ( - "import setuptools\n" - "setuptools.setup()\n" - )) - print("Wrote: setup.py") - - setupCfg = readFile("pyproject.toml").replace( - "setup/description_pypi.md", "data/description_short.txt" - ) - writeFile(f"{outDir}/pyproject.toml", setupCfg) - print("Wrote: pyproject.toml") + copyPackageFiles(outDir) # Write Metadata # ============== - appDescription = readFile("setup/description_short.txt") - appdataXML = readFile("setup/novelwriter.appdata.xml").format(description=appDescription) - writeFile(f"{imageDir}/novelwriter.appdata.xml", appdataXML) + appDescription = (SETUP_DIR / "description_short.txt").read_text() + appdataXML = (SETUP_DIR / "novelwriter.appdata.xml").read_text() + appdataXML = appdataXML.format(description=appDescription) + (imgDir / "novelwriter.appdata.xml").write_text(appdataXML) print("Wrote: novelwriter.appdata.xml") - writeFile(f"{imageDir}/entrypoint.sh", ( + (imgDir / "entrypoint.sh").write_text( '#! /bin/bash \n' '{{ python-executable }} -sE ${APPDIR}/opt/python{{ python-version }}/bin/novelwriter "$@"' - )) + ) print("Wrote: entrypoint.sh") - writeFile(f"{imageDir}/requirements.txt", os.path.abspath(outDir)) + (imgDir / "requirements.txt").write_text(str(outDir)) print("Wrote: requirements.txt") - shutil.copyfile("setup/data/novelwriter.desktop", f"{imageDir}/novelwriter.desktop") + shutil.copyfile(SETUP_DIR / "data" / "novelwriter.desktop", imgDir / "novelwriter.desktop") print("Copied: novelwriter.desktop") - shutil.copyfile("setup/icons/novelwriter.svg", f"{imageDir}/novelwriter.svg") + shutil.copyfile(SETUP_DIR / "icons" / "novelwriter.svg", imgDir / "novelwriter.svg") print("Copied: novelwriter.svg") shutil.copyfile( - "setup/data/hicolor/256x256/apps/novelwriter.png", f"{imageDir}/novelwriter.png" + SETUP_DIR / "data" / "hicolor" / "256x256" / "apps" / "novelwriter.png", + imgDir / "novelwriter.png" ) print("Copied: novelwriter.png") @@ -1081,15 +1014,15 @@ def makeAppImage(sysArgs: list[str]) -> list[str]: print("") sys.exit(1) - bldFile = glob.glob(f"{bldDir}/*.AppImage")[0] - outFile = f"{bldDir}/novelWriter-{pkgVers}.AppImage" - os.rename(bldFile, outFile) - shaFile = makeCheckSum(os.path.basename(outFile), cwd=bldDir) + 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 unparsedArgs + return ## @@ -1690,6 +1623,27 @@ if __name__ == "__main__": cmdBuildUbuntu.add_argument("--first", action="store_true", help="Set build number to 0.") cmdBuildUbuntu.set_defaults(func=buildForLaunchpad) + # Build 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.11." + ) + ) + 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.11", help="Python version (e.g. 3.11)" + ) + cmdBuildAppImage.set_defaults(func=buildAppImage) + # if "gen-plist" in sysArgs: # sysArgs.remove("gen-plist") # genMacOSPlist() From d43a79938991454a34762c5015eec0c66ed47f77 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 4 Jul 2024 21:00:15 +0200 Subject: [PATCH 04/11] Updated Windows packaging code --- pkgutils.py | 265 ++++++++++++-------------------------- setup/win_setup_embed.iss | 2 +- 2 files changed, 85 insertions(+), 182 deletions(-) diff --git a/pkgutils.py b/pkgutils.py index 7735dca4..04ec3b5d 100755 --- a/pkgutils.py +++ b/pkgutils.py @@ -616,78 +616,6 @@ def copyPackageFiles(dst: Path, useCfg: bool = False) -> None: return -## -# Make Minimal Package (minimal-zip) -## - -def makeWindowsZip() -> None: - """Pack the core source file in a single zip file.""" - from zipfile import ZIP_DEFLATED, ZipFile - - print("") - print("Building Windows ZIP File") - print("=========================") - - bldDir = "dist_minimal" - if not os.path.isdir(bldDir): - os.mkdir(bldDir) - - if not checkAssetsExist(): - print("ERROR: Missing build assets") - sys.exit(1) - - pkgVers, _, _ = extractVersion() - zipFile = f"novelwriter-{pkgVers}-minimal-win.zip" - outFile = os.path.join(bldDir, zipFile) - if os.path.isfile(outFile): - os.unlink(outFile) - print("") - - rootFiles = [ - "README.md", - "LICENSE.md", - "CREDITS.md", - "CHANGELOG.md", - "requirements.txt", - "pkgutils.py", - "pyproject.toml", - ] - - with ZipFile(outFile, "w", compression=ZIP_DEFLATED, compresslevel=9) as zipObj: - - for nRoot, _, nFiles in os.walk("novelwriter"): - if nRoot.endswith("__pycache__"): - print("Skipped: %s" % nRoot) - continue - - print("Added: %s/* [Files: %d]" % (nRoot, len(nFiles))) - for aFile in nFiles: - if aFile.endswith(".pyc"): - print("Skipping File: %s" % aFile) - continue - zipObj.write(os.path.join(nRoot, aFile)) - - zipObj.write("novelWriter.py", "novelWriter.pyw") - print("Added: novelWriter.pyw") - - for aFile in rootFiles: - print("Added: %s" % aFile) - zipObj.write(aFile) - - zipObj.write(os.path.join("novelwriter", "assets", "manual.pdf"), "UserManual.pdf") - print("Added: UserManual.pdf") - - print("") - print("Created File: %s" % outFile) - - shaFile = makeCheckSum(zipFile, cwd=bldDir) - toUpload(outFile) - toUpload(shaFile) - print("") - - return - - ## # Make Debian Package ## @@ -961,6 +889,7 @@ def buildAppImage(args: argparse.Namespace) -> None: print("Copying novelWriter source ...") print("") + copySourceCode(outDir) print("") @@ -1029,7 +958,7 @@ def buildAppImage(args: argparse.Namespace) -> None: # Make Windows Setup EXE (build-win-exe) ## -def makeWindowsEmbedded(sysArgs: list[str]) -> None: +def makeWindowsEmbedded(args: argparse.Namespace) -> None: """Set up a package with embedded Python and dependencies for Windows installation. """ @@ -1042,18 +971,8 @@ def makeWindowsEmbedded(sysArgs: list[str]) -> None: print("================================") print("") - minimalZip = None - packVersion = "none" - if len(sysArgs) >= 2: - if os.path.isfile(sysArgs[1]): - minimalZip = sysArgs[1] - - if minimalZip is None: - print("Please provide the path to the minimal win package as an argument") - sys.exit(1) - - packVersion = os.path.basename(minimalZip).split("-")[1] - print("Version: %s" % packVersion) + numVers, hexVers, relDate = extractVersion() + print("Version: %s" % numVers) # Set Up Folder # ============= @@ -1061,27 +980,36 @@ def makeWindowsEmbedded(sysArgs: list[str]) -> None: if not os.path.isdir("dist"): os.mkdir("dist") - outDir = os.path.join("dist", "novelWriter") - libDir = os.path.join(outDir, "lib") - if os.path.isdir(outDir): + bldDir = CURR_DIR / "dist" + outDir = bldDir / "novelWriter" + libDir = outDir / "lib" + if outDir.exists: shutil.rmtree(outDir) - os.mkdir(outDir) - os.mkdir(libDir) + outDir.mkdir() + libDir.mkdir() - # Extract Source Files - # ==================== + # Copy novelWriter Source + # ======================= - print("Extracting source files ...") - with zipfile.ZipFile(minimalZip, "r") as inFile: - inFile.extractall(outDir) + print("Copying and compiling novelWriter source ...") + print("") - shutil.copyfile( - os.path.join(outDir, "novelwriter", "assets", "icons", "novelwriter.ico"), - os.path.join(outDir, "novelwriter.ico") - ) + copySourceCode(outDir) - compileall.compile_dir(os.path.join(outDir, "novelwriter")) + files = [ + CURR_DIR / "CREDITS.md", + CURR_DIR / "LICENSE.md", + CURR_DIR / "requirements.txt", + SETUP_DIR / "icons" / "novelwriter.ico", + SETUP_DIR / "iss_license.txt", + + ] + for item in files: + shutil.copyfile(item, outDir) + print(f"Copied: {item} > {outDir / item.name}") + + compileall.compile_dir(outDir / "novelwriter") print("Done") print("") @@ -1106,28 +1034,16 @@ def makeWindowsEmbedded(sysArgs: list[str]) -> None: print("Done") print("") - # Sort Out Licence Files - # ====================== - - os.rename( - os.path.join(outDir, "LICENSE.txt"), - os.path.join(outDir, "PYTHON-LICENSE.txt") - ) - shutil.copyfile( - os.path.join("setup", "iss_license.txt"), - os.path.join(outDir, "LICENSES.txt") - ) - # Install Dependencies # ==================== print("Install dependencies ...") - sysCmd = [sys.executable] - sysCmd += "-m pip install -r requirements.txt --target".split() - sysCmd += [libDir] try: - subprocess.call(sysCmd) + subprocess.call([ + sys.executable, "-m", + "pip", "install", "-r", "requirements.txt", "--target", str(libDir) + ]) except Exception as exc: print("Failed with error:") print(str(exc)) @@ -1141,7 +1057,7 @@ def makeWindowsEmbedded(sysArgs: list[str]) -> None: print("Updating starting script ...") - writeFile(os.path.join(outDir, "novelWriter.pyw"), ( + (outDir / "novelWriter.pyw").write_text( "#!/usr/bin/env python3\n" "import os\n" "import sys\n" @@ -1152,7 +1068,7 @@ def makeWindowsEmbedded(sysArgs: list[str]) -> None: "if __name__ == \"__main__\":\n" " import novelwriter\n" " novelwriter.main(sys.argv[1:])\n" - )) + ) print("Done") print("") @@ -1160,35 +1076,35 @@ def makeWindowsEmbedded(sysArgs: list[str]) -> None: # Clean Up Files # ============== - def unlinkIfFound(delFile: str) -> None: - if os.path.isfile(delFile): - os.unlink(delFile) - print("Deleted: %s" % delFile) + def unlinkIfFound(file: Path) -> None: + if file.is_file(): + file.unlink() + print(f"Deleted: {file}") - def deleteFolder(delPath: str) -> None: - if os.path.isdir(delPath): - shutil.rmtree(delPath) - print("Deleted: %s" % delPath) + def deleteFolder(folder: Path) -> None: + if folder.is_dir(): + shutil.rmtree(folder) + print(f"Deleted: {folder}") print("Deleting Redundant Files") print("========================") print("") - pyQt5Dir = os.path.join(libDir, "PyQt5") - bindDir = os.path.join(pyQt5Dir, "bindings") - qt5Dir = os.path.join(pyQt5Dir, "Qt5") - binDir = os.path.join(qt5Dir, "bin") - plugDir = os.path.join(qt5Dir, "plugins") - qmDir = os.path.join(qt5Dir, "translations") - dictDir = os.path.join(libDir, "enchant", "data", "mingw64", "share", "enchant", "hunspell") + pyQt5Dir = libDir / "PyQt5" + bindDir = pyQt5Dir / "bindings" + qt5Dir = pyQt5Dir / "Qt5" + binDir = qt5Dir / "bin" + plugDir = qt5Dir / "plugins" + qmDir = qt5Dir / "translations" + dictDir = libDir / "enchant" / "data" / "mingw64" / "share" / "enchant" / "hunspell" - for dictFile in os.listdir(dictDir): - if not dictFile.startswith(("en_GB", "en_US")): - unlinkIfFound(os.path.join(dictDir, dictFile)) + for item in dictDir.iterdir(): + if not item.name.startswith(("en_GB", "en_US")): + unlinkIfFound(item) - for qmFile in os.listdir(qmDir): - if not qmFile.startswith("qtbase"): - unlinkIfFound(os.path.join(qmDir, qmFile)) + for item in qmDir.iterdir(): + if not item.name.startswith("qtbase"): + unlinkIfFound(item) delQt5 = [ "Qt5Bluetooth", "Qt5DBus", "Qt5Designer", "Qt5Designer", "Qt5Help", "Qt5Location", @@ -1200,30 +1116,28 @@ def makeWindowsEmbedded(sysArgs: list[str]) -> None: "Qt5SerialPort", "Qt5Sql", "Qt5Test", "Qt5TextToSpeech", "Qt5WebChannel", "Qt5WebSockets", "Qt5WebView", "Qt5Xml", "Qt5XmlPatterns" ] - for qt5Item in delQt5: - qtItem = qt5Item.replace("Qt5", "Qt") - unlinkIfFound(os.path.join(binDir, qt5Item+".dll")) - unlinkIfFound(os.path.join(pyQt5Dir, qtItem+".pyd")) - unlinkIfFound(os.path.join(pyQt5Dir, qtItem+".pyi")) - deleteFolder(os.path.join(bindDir, qtItem)) + for item in delQt5: + qtItem = item.replace("Qt5", "Qt") + unlinkIfFound(binDir / f"{item}.dll") + unlinkIfFound(pyQt5Dir / f"{qtItem}.pyd") + unlinkIfFound(pyQt5Dir / f"{qtItem}.pyi") + deleteFolder(bindDir / qtItem) delList = [ - os.path.join(binDir, "opengl32sw.dll"), - os.path.join(qt5Dir, "qml"), - os.path.join(plugDir, "geoservices"), - os.path.join(plugDir, "playlistformats"), - os.path.join(plugDir, "renderers"), - os.path.join(plugDir, "sensorgestures"), - os.path.join(plugDir, "sensors"), - os.path.join(plugDir, "sqldrivers"), - os.path.join(plugDir, "texttospeech"), - os.path.join(plugDir, "webview"), + binDir / "opengl32sw.dll", + qt5Dir / "qml", + plugDir / "geoservices", + plugDir / "playlistformats", + plugDir / "renderers", + plugDir / "sensorgestures", + plugDir / "sensors", + plugDir / "sqldrivers", + plugDir / "texttospeech", + plugDir / "webview", ] - for delItem in delList: - if os.path.isfile(delItem): - unlinkIfFound(delItem) - elif os.path.isdir(delItem): - deleteFolder(delItem) + for item in delList: + unlinkIfFound(item) + deleteFolder(item) print("Done") print("") @@ -1233,10 +1147,10 @@ def makeWindowsEmbedded(sysArgs: list[str]) -> None: print("") # Read the iss template - issData = readFile(os.path.join("setup", "win_setup_embed.iss")) - issData = issData.replace(r"%%version%%", packVersion) - issData = issData.replace(r"%%dir%%", os.getcwd()) - writeFile("setup.iss", issData) + issData = (SETUP_DIR / "win_setup_embed.iss").read_text() + issData = issData.replace(r"%%version%%", numVers) + issData = issData.replace(r"%%dist%%", str(bldDir)) + (CURR_DIR / "setup.iss").write_text(issData) print("") try: @@ -1644,27 +1558,16 @@ if __name__ == "__main__": ) cmdBuildAppImage.set_defaults(func=buildAppImage) + # Build Windows Inno Setup Installer + cmdBuildSetupExe = parsers.add_parser( + "build-win-exe", help="Build a setup.exe file with Python embedded for Windows." + ) + cmdBuildSetupExe.set_defaults(func=makeWindowsEmbedded) + # if "gen-plist" in sysArgs: # sysArgs.remove("gen-plist") # genMacOSPlist() - # if "windows-zip" in sysArgs: - # sysArgs.remove("windows-zip") - # makeWindowsZip() - - # if "build-win-exe" in sysArgs: - # sysArgs.remove("build-win-exe") - # makeWindowsEmbedded(sysArgs) - # sys.exit(0) # Don't continue execution - - # 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) - # # General Installers # # ================== diff --git a/setup/win_setup_embed.iss b/setup/win_setup_embed.iss index dc856f69..042914fc 100644 --- a/setup/win_setup_embed.iss +++ b/setup/win_setup_embed.iss @@ -1,6 +1,6 @@ ; Script for building setup.exe installer with Inno Setup -#define nwAppDir "%%dir%%\dist" +#define nwAppDir "%%dist%%" #define nwAppName "novelWriter" #define nwAppVersion "%%version%%" #define nwAppPublisher "novelWriter" From 43eb62576c501c631e6b42de29cc5986fbf05a3f Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 4 Jul 2024 21:16:49 +0200 Subject: [PATCH 05/11] Fix Windows installer and update MacOS PList generator --- pkgutils.py | 66 +++++++++++++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/pkgutils.py b/pkgutils.py index 04ec3b5d..94a83a5c 100755 --- a/pkgutils.py +++ b/pkgutils.py @@ -520,33 +520,6 @@ def buildAllAssets(args: argparse.Namespace) -> None: return -## -# Generate MacOS PList -## - -def genMacOSPlist() -> None: - """Set necessary values for .plist file for MacOS build.""" - outDir = "setup/macos" - numVers = stripVersion(extractVersion()[0]) - copyrightYear = datetime.datetime.now().year - - # These keys are no longer used but are present for compatibility - pkgVersMaj, pkgVersMin = numVers.split(".")[:2] - - plistXML = readFile(f"{outDir}/Info.plist.template").format( - macosBundleSVers=numVers, - macosBundleVers=numVers, - macosBundleVersMajor=pkgVersMaj, - macosBundleVersMinor=pkgVersMin, - macosBundleCopyright=f"Copyright 2018–{copyrightYear}, Veronica Berglyd Olsen", - ) - - print(f"Writing Info.plist to {outDir}/Info.plist") - writeFile(f"{outDir}/Info.plist", plistXML) - - return - - # =============================================================================================== # # Python Packaging # =============================================================================================== # @@ -971,7 +944,7 @@ def makeWindowsEmbedded(args: argparse.Namespace) -> None: print("================================") print("") - numVers, hexVers, relDate = extractVersion() + numVers, _, _ = extractVersion() print("Version: %s" % numVers) # Set Up Folder @@ -1006,7 +979,7 @@ def makeWindowsEmbedded(args: argparse.Namespace) -> None: ] for item in files: - shutil.copyfile(item, outDir) + shutil.copyfile(item, outDir / item.name) print(f"Copied: {item} > {outDir / item.name}") compileall.compile_dir(outDir / "novelwriter") @@ -1167,6 +1140,33 @@ def makeWindowsEmbedded(args: argparse.Namespace) -> None: return +## +# Generate MacOS PList +## + +def genMacOSPlist(args: argparse.Namespace) -> None: + """Set necessary values for .plist file for MacOS build.""" + outDir = SETUP_DIR / "macos" + numVers = stripVersion(extractVersion()[0]) + copyrightYear = datetime.datetime.now().year + + # These keys are no longer used but are present for compatibility + pkgVersMaj, pkgVersMin = numVers.split(".")[:2] + + plistXML = (outDir / "Info.plist.template").read_text().format( + macosBundleSVers=numVers, + macosBundleVers=numVers, + macosBundleVersMajor=pkgVersMaj, + macosBundleVersMinor=pkgVersMin, + macosBundleCopyright=f"Copyright 2018–{copyrightYear}, Veronica Berglyd Olsen", + ) + + print(f"Writing Info.plist to {outDir}/Info.plist") + (outDir / "Info.plist").write_text(plistXML) + + return + + # =============================================================================================== # # General Installers # =============================================================================================== # @@ -1564,9 +1564,11 @@ if __name__ == "__main__": ) cmdBuildSetupExe.set_defaults(func=makeWindowsEmbedded) - # if "gen-plist" in sysArgs: - # sysArgs.remove("gen-plist") - # genMacOSPlist() + # Generate MacOS PList File + cmdBuildMacOSPlist = parsers.add_parser( + "gen-plist", help="Generate an Info.plist for use in a MacOS Bundle." + ) + cmdBuildMacOSPlist.set_defaults(func=genMacOSPlist) # # General Installers # # ================== From 1c0fdd6a17e0ba35f162f690c08e7d2d1c1ae580 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 4 Jul 2024 21:38:31 +0200 Subject: [PATCH 06/11] Update XDG tools, clean up and fix a deb build issue --- pkgutils.py | 155 ++++++++++++++++++---------------------------------- 1 file changed, 54 insertions(+), 101 deletions(-) diff --git a/pkgutils.py b/pkgutils.py index 94a83a5c..3a38221e 100755 --- a/pkgutils.py +++ b/pkgutils.py @@ -28,7 +28,6 @@ from __future__ import annotations import argparse import datetime import email.utils -import os import shutil import subprocess import sys @@ -113,22 +112,22 @@ def toUpload(srcPath: str | Path, dstName: str | None = None) -> None: return -def makeCheckSum(sumFile: str, cwd: Path | str | None = None) -> str: +def makeCheckSum(sumFile: str, cwd: Path | None = None) -> str: """Create a SHA256 checksum file.""" try: if cwd is None: - shaFile = sumFile+".sha256" + shaFile = f"{sumFile}.sha256" else: - shaFile = os.path.join(cwd, sumFile+".sha256") + shaFile = cwd / f"{sumFile}.sha256" with open(shaFile, mode="w") as fOut: subprocess.call(["shasum", "-a", "256", sumFile], stdout=fOut, cwd=cwd) - print("SHA256 Sum: %s" % shaFile) + print(f"SHA256 Sum: {shaFile}") except Exception as exc: print("Could not generate sha256 file") print(str(exc)) return "" - return shaFile + return str(shaFile) def checkAssetsExist() -> bool: @@ -137,22 +136,20 @@ def checkAssetsExist() -> bool: hasManual = False hasQmData = False - sampleZip = os.path.join("novelwriter", "assets", "sample.zip") - if os.path.isfile(sampleZip): + sampleZip = CURR_DIR / "novelwriter" / "assets" / "sample.zip" + if sampleZip.is_file(): print(f"Found: {sampleZip}") hasSample = True - pdfManual = os.path.join("novelwriter", "assets", "manual.pdf") - if os.path.isfile(pdfManual): + pdfManual = CURR_DIR / "novelwriter" / "assets" / "manual.pdf" + if pdfManual.is_file(): print(f"Found: {pdfManual}") hasManual = True - 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"Found: {i18nPath}") - hasQmData = True + i18nAssets = CURR_DIR / "novelwriter" / "assets" / "i18n" + if len(list(i18nAssets.glob("*.qm"))) > 0: + print(f"Found: {i18nAssets}/*.qm") + hasQmData = True return hasSample and hasManual and hasQmData @@ -283,7 +280,7 @@ def buildPdfManual(args: argparse.Namespace | None = None) -> None: print("") sys.exit(1) - if not os.path.isfile(finalFile): + if not finalFile.is_file(): print("No output file was found!") print("") sys.exit(1) @@ -551,7 +548,7 @@ def copySourceCode(dst: Path) -> None: # Copy Package Files ## -def copyPackageFiles(dst: Path, useCfg: bool = False) -> None: +def copyPackageFiles(dst: Path, setupPy: bool = False, useCfg: bool = False) -> None: """Copy files needed for packaging.""" copyFiles = ["LICENSE.md", "CREDITS.md", "pyproject.toml"] @@ -562,11 +559,17 @@ def copyPackageFiles(dst: Path, useCfg: bool = False) -> None: (dst / "MANIFEST.in").write_text( "include LICENSE.md\n" "include CREDITS.md\n" - # "include data/*\n" "recursive-include novelwriter/assets *\n" ) print("Wrote: MANIFEST.in") + if setupPy: + (dst / "setup.py").write_text( + "import setuptools\n" + "setuptools.setup()\n" + ) + print("Wrote: setup.py") + if useCfg: # This is needed for Ubuntu up to 22.04 text = (SETUP_DIR / "launchpad_setup.cfg").read_text() @@ -655,7 +658,7 @@ def makeDebianPackage( print("Copying or generating additional files ...") print("") - copyPackageFiles(outDir, oldSetuptools) + copyPackageFiles(outDir, setupPy=True, useCfg=oldSetuptools) # Copy/Write Debian Files # ======================= @@ -950,15 +953,13 @@ def makeWindowsEmbedded(args: argparse.Namespace) -> None: # Set Up Folder # ============= - if not os.path.isdir("dist"): - os.mkdir("dist") - bldDir = CURR_DIR / "dist" outDir = bldDir / "novelWriter" libDir = outDir / "lib" if outDir.exists: shutil.rmtree(outDir) + bldDir.mkdir(exist_ok=True) outDir.mkdir() libDir.mkdir() @@ -993,9 +994,9 @@ def makeWindowsEmbedded(args: argparse.Namespace) -> None: print("Adding Python embeddable ...") pyVers = "%d.%d.%d" % (sys.version_info[:3]) - zipFile = "python-%s-embed-amd64.zip" % pyVers - pyZip = os.path.join("dist", zipFile) - if not os.path.isfile(pyZip): + 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) @@ -1175,7 +1176,7 @@ def genMacOSPlist(args: argparse.Namespace) -> None: # XDG Installation (xdg-install) ## -def xdgInstall() -> None: +def xdgInstall(args: argparse.Namespace) -> None: """Will attempt to install icons and make a launcher.""" print("") print("XDG Install") @@ -1195,9 +1196,9 @@ def xdgInstall() -> None: if testExec is not None: exOpts.append(testExec) - testExec = os.path.join(os.getcwd(), "novelWriter.py") - if os.path.isfile(testExec): - exOpts.append(testExec) + testExec = CURR_DIR / "novelWriter.py" + if testExec.is_file(): + exOpts.append(str(testExec)) useExec = "" nOpts = len(exOpts) @@ -1228,9 +1229,10 @@ def xdgInstall() -> None: # =========================== # Generate launcher - desktopData = readFile(os.path.join("setup", "data", "novelwriter.desktop")) + desktopFile = CURR_DIR / "novelwriter.desktop" + desktopData = (SETUP_DIR / "data" / "novelwriter.desktop").read_text() desktopData = desktopData.replace("Exec=novelwriter", f"Exec={useExec}") - writeFile("novelwriter.desktop", desktopData) + desktopFile.write_text(desktopData) # Remove old desktop icon exCode = subprocess.call( @@ -1297,8 +1299,7 @@ def xdgInstall() -> None: print(f"Error {exCode}: Could not update icon cache") # Clean up - if os.path.isfile("./novelwriter.desktop"): - os.unlink("./novelwriter.desktop") + desktopFile.unlink(missing_ok=True) print("") print("Done!") @@ -1311,7 +1312,7 @@ def xdgInstall() -> None: # XDG Uninstallation (xdg-uninstall) ## -def xdgUninstall() -> None: +def xdgUninstall(args: argparse.Namespace) -> None: """Will attempt to uninstall icons and the launcher.""" print("") print("XDG Uninstall") @@ -1386,56 +1387,6 @@ if __name__ == "__main__": parser = argparse.ArgumentParser() parsers = parser.add_subparsers() - helpMsg = [ - "", - "novelWriter Setup Tool", - "======================", - "", - "This tool provides setup and build commands for installing or distibuting", - "novelWriter as a package on Linux, Mac and Windows. The available options", - "are as follows:", - "", - "General:", - "", - " help Print the help message.", - " pip Install all package dependencies for novelWriter using pip.", - " version Print the novelWriter version.", - " build-clean Will attempt to delete 'build' and 'dist' folders.", - "", - "Additional Builds:", - "", - " manual Build the help documentation as PDF (requires LaTeX).", - " sample Build the sample project zip file and add it to assets.", - " qtlupdate Update translation files for internationalisation.", - " The files to be updated must be provided as arguments.", - " qtlrelease Build the language files for internationalisation.", - " clean-assets Delete assets built by manual, sample and qtlrelease.", - " gen-plist Generates an Info.plist for use in a MacOS Bundle", - "", - "Python Packaging:", - "", - " import-i18n Import updated i18n files from a zip file.", - " windows-zip Creates a minimal zip file of the core application without", - " all the other source files. Used for Windows builds.", - " build-deb Build a .deb package for Debian and Ubuntu. Add --sign to ", - " sign package.", - " build-ubuntu Build a .deb packages Launchpad. Add --sign to ", - " sign package. Add --first to set build number to 0.", - " build-win-exe Build a setup.exe file with Python embedded for Windows.", - " The package must be built from a minimal windows zip file.", - " build-appimage Build an AppImage. Argument --linux-tag defaults to", - " manylinux_2_28_x86_64, and --python-version to 3.11.", - "", - "System Install:", - "", - " xdg-install Install launcher and icons for freedesktop systems. Run as", - " root or with sudo for system-wide install, or as user for", - " single user install.", - " xdg-uninstall Remove the launcher and icons for the current system as", - " installed by the 'xdg-install' command.", - "", - ] - # Version cmdVersion = parsers.add_parser( "version", help="Print the novelWriter version." @@ -1570,24 +1521,26 @@ if __name__ == "__main__": ) cmdBuildMacOSPlist.set_defaults(func=genMacOSPlist) - # # General Installers - # # ================== + # General Installers + # ================== - # if "xdg-install" in sysArgs: - # sysArgs.remove("xdg-install") - # if hostOS == OS_WIN: - # print("ERROR: Command 'xdg-install' cannot be used on Windows") - # sys.exit(1) - # else: - # xdgInstall() + # Linux XDG Install + cmdXDGInstall = parsers.add_parser( + "xdg-install", help=( + "Install launcher and icons for freedesktop systems. Run as root or with sudo for " + "system-wide install, or as user for single user install." + ) + ) + cmdXDGInstall.set_defaults(func=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() + # Linux XDG Uninstall + cmdXDGUninstall = parsers.add_parser( + "xdg-uninstall", help=( + "Remove the launcher and icons for the current system " + "as installed by the 'xdg-install' command." + ) + ) + cmdXDGUninstall.set_defaults(func=xdgUninstall) args = parser.parse_args() args.func(args) From eba0fa58bfa7fc5245e9c2222dcd595c41c554d1 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 4 Jul 2024 21:40:48 +0200 Subject: [PATCH 07/11] Update Windows build job --- .github/workflows/build_win.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/build_win.yml b/.github/workflows/build_win.yml index c16e5340..086b7103 100644 --- a/.github/workflows/build_win.yml +++ b/.github/workflows/build_win.yml @@ -15,5 +15,23 @@ jobs: with: python-version: "3.12" architecture: x64 + - name: Checkout Source uses: actions/checkout@v4 + + - name: Download Artifacts + uses: actions/download-artifact@v4 + with: + name: nw-assets + path: novelwriter/assets + + - name: Build Setup Installer + run: python pkgutils.py build-win-exe + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + name: Win-Setup + path: dist/*.exe + if-no-files-found: error + retention-days: 14 From e40e282837df383ccc86dd4ed4ff4cff2235bede Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 4 Jul 2024 21:44:28 +0200 Subject: [PATCH 08/11] Fix bug in Windows build script --- pkgutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgutils.py b/pkgutils.py index 3a38221e..cd8cf0a9 100755 --- a/pkgutils.py +++ b/pkgutils.py @@ -956,7 +956,7 @@ def makeWindowsEmbedded(args: argparse.Namespace) -> None: bldDir = CURR_DIR / "dist" outDir = bldDir / "novelWriter" libDir = outDir / "lib" - if outDir.exists: + if outDir.exists(): shutil.rmtree(outDir) bldDir.mkdir(exist_ok=True) From cc97129e6433103ba560b3b8710539494fe93d6b Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 4 Jul 2024 22:01:52 +0200 Subject: [PATCH 09/11] Add description to pkgutils help --- pkgutils.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkgutils.py b/pkgutils.py index cd8cf0a9..69058254 100755 --- a/pkgutils.py +++ b/pkgutils.py @@ -1384,7 +1384,14 @@ def xdgUninstall(args: argparse.Namespace) -> None: if __name__ == "__main__": """Parse command line options and run the commands.""" - parser = argparse.ArgumentParser() + parser = argparse.ArgumentParser( + usage="pkgutils.py [command] [--flags]", + description=( + "This tool provides setup and build commands for installing or distibuting " + "novelWriter as a package on Linux, Mac and Windows, as well as developer tools " + "for internationalisation." + ) + ) parsers = parser.add_subparsers() # Version From 47b8e12bc7de6167d8f4f21877318d25371a9dfe Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 4 Jul 2024 22:07:33 +0200 Subject: [PATCH 10/11] Fix a few workflow issues --- .github/workflows/build_assets.yml | 4 +--- .github/workflows/test_linux.yml | 2 +- setup/make_release.sh | 6 ------ 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build_assets.yml b/.github/workflows/build_assets.yml index 05579f59..2889ae5d 100644 --- a/.github/workflows/build_assets.yml +++ b/.github/workflows/build_assets.yml @@ -25,9 +25,7 @@ jobs: - name: Build Assets run: | - python pkgutils.py manual - python pkgutils.py sample - python pkgutils.py qtlrelease + python pkgutils.py build-assets - name: Upload Artifacts uses: actions/upload-artifact@v4 diff --git a/.github/workflows/test_linux.yml b/.github/workflows/test_linux.yml index 897d1c0e..955b798a 100644 --- a/.github/workflows/test_linux.yml +++ b/.github/workflows/test_linux.yml @@ -33,7 +33,7 @@ jobs: run: | pip install -U -r requirements.txt -r tests/requirements.txt - name: Run Build Commands - run: python pkgutils.py qtlrelease sample + run: python pkgutils.py build-assets - name: Run Tests run: | export QT_QPA_PLATFORM=offscreen diff --git a/setup/make_release.sh b/setup/make_release.sh index c5237745..d88340a7 100755 --- a/setup/make_release.sh +++ b/setup/make_release.sh @@ -20,12 +20,6 @@ pip3 install -r docs/source/requirements.txt python3 pkgutils.py build-assets deactivate -echo "" -echo " Building Windows Source Zip" -echo "================================================================================" -echo "" -python3 pkgutils.py windows-zip - echo "" echo " Building Linux Packages" echo "================================================================================" From 5f3f55133e8b2eb90f44b10097c83b65251f8852 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 4 Jul 2024 22:08:28 +0200 Subject: [PATCH 11/11] Remove manual build from linux tests --- .github/workflows/test_linux.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_linux.yml b/.github/workflows/test_linux.yml index 955b798a..5ff6cab5 100644 --- a/.github/workflows/test_linux.yml +++ b/.github/workflows/test_linux.yml @@ -33,7 +33,9 @@ jobs: run: | pip install -U -r requirements.txt -r tests/requirements.txt - name: Run Build Commands - run: python pkgutils.py build-assets + run: | + python pkgutils.py qtlrelease + python pkgutils.py sample - name: Run Tests run: | export QT_QPA_PLATFORM=offscreen