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()