Updated AppImage packaging code

This commit is contained in:
Veronica Berglyd Olsen
2024-07-04 20:31:53 +02:00
parent 7c65d22452
commit 4d716555af
+153 -199
View File
@@ -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()