Updated debian packaging code

This commit is contained in:
Veronica Berglyd Olsen
2024-07-04 19:45:54 +02:00
parent 1b8867537e
commit 7c65d22452
+166 -190
View File
@@ -36,12 +36,12 @@ import zipfile
from pathlib import Path from pathlib import Path
OS_NONE = 0
OS_LINUX = 1
OS_WIN = 2
OS_DARWIN = 3
CURR_DIR = Path(__file__).parent 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 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: def readFile(fileName: str) -> str:
"""Read an entire file and return as a string.""" """Read an entire file and return as a string."""
return Path(fileName).read_text(encoding="utf-8") return Path(fileName).read_text(encoding="utf-8")
@@ -112,7 +133,7 @@ def toUpload(srcPath: str | Path, dstName: str | None = None) -> None:
return 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.""" """Create a SHA256 checksum file."""
try: try:
if cwd is None: if cwd is None:
@@ -130,6 +151,32 @@ def makeCheckSum(sumFile: str, cwd: str | None = None) -> str:
return shaFile 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 # General
# =============================================================================================== # # =============================================================================================== #
@@ -484,33 +531,6 @@ def genMacOSPlist() -> None:
return 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 # Python Packaging
# =============================================================================================== # # =============================================================================================== #
@@ -519,36 +539,32 @@ def checkAssetsExist() -> bool:
# Import Translations (import-i18n) # Import Translations (import-i18n)
## ##
def importI18nUpdates(sysArgs: list[str]) -> None: def importI18nUpdates(args: argparse.Namespace) -> None:
"""Import new translation files from a zip file.""" """Import new translation files from a zip file."""
print("") print("")
print("Import Updated Translations") print("Import Updated Translations")
print("===========================") print("===========================")
print("") print("")
fileName = None fileName = Path(args.file).absolute()
if len(sysArgs) >= 2: if not fileName.is_file():
if os.path.isfile(sysArgs[1]):
fileName = sysArgs[1]
if fileName is None:
print("File not found ...") print("File not found ...")
sys.exit(1) sys.exit(1)
projPath = os.path.join("novelwriter", "assets", "i18n") dstPath = CURR_DIR / "novelwriter" / "assets" / "i18n"
mainPath = "i18n" srcPath = CURR_DIR / "i18n"
print("Loading file: %s" % fileName) print(f"Loading file: {fileName}")
with zipfile.ZipFile(fileName) as zipObj: with zipfile.ZipFile(fileName) as zipObj:
for archFile in zipObj.namelist(): for item in zipObj.namelist():
if archFile.startswith("nw_") and archFile.endswith(".ts"): if item.startswith("nw_") and item.endswith(".ts"):
zipObj.extract(archFile, mainPath) zipObj.extract(item, srcPath)
print("Extracted: %s > %s" % (archFile, os.path.join(mainPath, archFile))) print(f"Extracted: {item} > {srcPath / item}")
elif archFile.startswith("project_") and archFile.endswith(".json"): elif item.startswith("project_") and item.endswith(".json"):
zipObj.extract(archFile, projPath) zipObj.extract(item, dstPath)
print("Extracted: %s > %s" % (archFile, os.path.join(projPath, archFile))) print(f"Extracted: {item} > {dstPath / item}")
else: else:
print("Skipped: %s" % archFile) print(f"Skipped: {item}")
print("") print("")
@@ -628,7 +644,7 @@ def makeWindowsZip() -> None:
## ##
# Make Debian Package (build-deb) # Make Debian Package
## ##
def makeDebianPackage( def makeDebianPackage(
@@ -660,21 +676,20 @@ def makeDebianPackage(
# Set Up Folder # Set Up Folder
# ============= # =============
bldDir = "dist_deb" refDir = CURR_DIR / "setup"
bldDir = CURR_DIR / "dist_deb"
bldPkg = f"novelwriter_{pkgVers}" bldPkg = f"novelwriter_{pkgVers}"
outDir = f"{bldDir}/{bldPkg}" outDir = bldDir / bldPkg
debDir = f"{outDir}/debian" debDir = outDir / "debian"
datDir = f"{outDir}/data" datDir = outDir / "data"
if not os.path.isdir(bldDir): bldDir.mkdir(exist_ok=True)
os.mkdir(bldDir) if outDir.exists():
if os.path.isdir(outDir):
print("Removing old build files ...") print("Removing old build files ...")
print("") print("")
shutil.rmtree(outDir) shutil.rmtree(outDir)
os.mkdir(outDir) outDir.mkdir(exist_ok=False)
# Check Additional Assets # Check Additional Assets
# ======================= # =======================
@@ -688,29 +703,7 @@ def makeDebianPackage(
print("Copying novelWriter source ...") print("Copying novelWriter source ...")
print("") print("")
copySourceCode(outDir)
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))
print("") print("")
print("Copying or generating additional files ...") print("Copying or generating additional files ...")
@@ -724,63 +717,60 @@ def makeDebianPackage(
shutil.copyfile(copyFile, f"{outDir}/{copyFile}") shutil.copyfile(copyFile, f"{outDir}/{copyFile}")
print("Copied: %s" % copyFile) print("Copied: %s" % copyFile)
writeFile(f"{outDir}/MANIFEST.in", ( (outDir / "MANIFEST.in").write_text(
"include LICENSE.md\n" "include LICENSE.md\n"
"include CREDITS.md\n" "include CREDITS.md\n"
"include CHANGELOG.md\n" "include CHANGELOG.md\n"
"include data/*\n" "include data/*\n"
"recursive-include novelwriter/assets *\n" "recursive-include novelwriter/assets *\n"
)) )
print("Wrote: MANIFEST.in") print("Wrote: MANIFEST.in")
writeFile(f"{outDir}/setup.py", ( (outDir / "setup.py").write_text(
"import setuptools\n" "import setuptools\n"
"setuptools.setup()\n" "setuptools.setup()\n"
)) )
print("Wrote: setup.py") print("Wrote: setup.py")
if oldSetuptools: if oldSetuptools:
# This is needed for Ubuntu up to 22.04 # This is needed for Ubuntu up to 22.04
setupCfg = readFile("setup/launchpad_setup.cfg").replace( text = (CURR_DIR / "setup" / "launchpad_setup.cfg").read_text()
"file: setup/description_pypi.md", "file: data/description_short.txt" text.replace("setup/description_pypi.md", "data/description_short.txt")
) (outDir / "setup.cfg").write_text(text)
writeFile(f"{outDir}/setup.cfg", setupCfg)
print("Wrote: setup.cfg") print("Wrote: setup.cfg")
writeFile(f"{outDir}/pyproject.toml", ( (outDir / "pyproject.toml").write_text(
"[build-system]\n" "[build-system]\n"
"requires = [\"setuptools\"]\n" "requires = [\"setuptools\"]\n"
"build-backend = \"setuptools.build_meta\"\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") print("Wrote: pyproject.toml")
# Copy/Write Debian Files # Copy/Write Debian Files
# ======================= # =======================
shutil.copytree("setup/debian", debDir) shutil.copytree(refDir / "debian", debDir)
print("Copied: debian/*") print("Copied: debian/*")
writeFile(f"{debDir}/changelog", ( (debDir / "changelog").write_text(
f"novelwriter ({pkgVers}) {distName}; urgency=low\n\n" f"novelwriter ({pkgVers}) {distName}; urgency=low\n\n"
f" * Update to version {pkgVers}\n\n" f" * Update to version {pkgVers}\n\n"
f" -- Veronica Berglyd Olsen <code@vkbo.net> {pkgDate}\n" f" -- Veronica Berglyd Olsen <code@vkbo.net> {pkgDate}\n"
)) )
print("Wrote: debian/changelog") print("Wrote: debian/changelog")
# Copy/Write Data Files # Copy/Write Data Files
# ===================== # =====================
shutil.copytree("setup/data", datDir) shutil.copytree(refDir / "data", datDir)
print("Copied: data/*") 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") print("Copied: data/description_short.txt")
# Build Package # Build Package
@@ -797,13 +787,12 @@ def makeDebianPackage(
if sourceBuild: if sourceBuild:
subprocess.call(["debuild", "-S"] + signArgs, cwd=outDir) subprocess.call(["debuild", "-S"] + signArgs, cwd=outDir)
toUpload(f"{bldDir}/{bldPkg}.tar.xz") toUpload(bldDir / f"{bldPkg}.tar.xz")
else: else:
subprocess.call(["dpkg-buildpackage"] + signArgs, cwd=outDir) subprocess.call(["dpkg-buildpackage"] + signArgs, cwd=outDir)
shutil.copyfile(f"{bldDir}/{bldPkg}.tar.xz", f"{bldDir}/{bldPkg}.debian.tar.xz") shutil.copyfile(bldDir / f"{bldPkg}.tar.xz", bldDir / f"{bldPkg}.debian.tar.xz")
toUpload(f"{bldDir}/{bldPkg}.debian.tar.xz") toUpload(bldDir / f"{bldPkg}.debian.tar.xz")
toUpload(f"{bldDir}/{bldPkg}_all.deb") toUpload(bldDir / f"{bldPkg}_all.deb")
toUpload(makeCheckSum(f"{bldPkg}.debian.tar.xz", cwd=bldDir)) toUpload(makeCheckSum(f"{bldPkg}.debian.tar.xz", cwd=bldDir))
toUpload(makeCheckSum(f"{bldPkg}_all.deb", 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.""" """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("")
print("Launchpad Packages") print("Launchpad Packages")
print("==================") print("==================")
print("") print("")
if isFirst: if args.first:
bldNum = "0" bldNum = "0"
else: else:
bldNum = input("Build number [0]: ") 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(f" * Ubuntu {distNum} {codeName.title()}")
print("") print("")
if doSign: signKey = SIGN_KEY if args.sign else None
signKey = "D6A9F6B8F227CF7C6F6D1EE84DBBE4B734B0BD08"
else:
signKey = None
print(f"Sign Key: {str(signKey)}") print(f"Sign Key: {str(signKey)}")
print("") print("")
@@ -1532,41 +1536,9 @@ def xdgUninstall() -> None:
if __name__ == "__main__": if __name__ == "__main__":
"""Parse command line options and run the commands.""" """Parse command line options and run the commands."""
# Detect OS
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() parser = argparse.ArgumentParser()
parsers = parser.add_subparsers() 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 = [ helpMsg = [
"", "",
"novelWriter Setup Tool", "novelWriter Setup Tool",
@@ -1630,9 +1602,9 @@ if __name__ == "__main__":
cmdPipInstall = parsers.add_parser( cmdPipInstall = parsers.add_parser(
"pip", help="Install all package dependencies for novelWriter using pip." "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("--linux", action="store_true", help="For Linux.", default=OS_LINUX)
cmdPipInstall.add_argument("--mac", action="store_true", help="For MacOS.", default=isMacOS) cmdPipInstall.add_argument("--mac", action="store_true", help="For MacOS.", default=OS_DARWIN)
cmdPipInstall.add_argument("--win", action="store_true", help="For Windows.", default=isWin) cmdPipInstall.add_argument("--win", action="store_true", help="For Windows.", default=OS_WIN)
cmdPipInstall.set_defaults(func=installPackages) cmdPipInstall.set_defaults(func=installPackages)
# Build Clean # Build Clean
@@ -1644,17 +1616,12 @@ if __name__ == "__main__":
# Additional Builds # Additional Builds
# ================= # =================
# Build Manual # Import Translations
cmdBuildManual = parsers.add_parser( cmdImportTS = parsers.add_parser(
"manual", help="Build the help documentation as a PDF (requires LaTeX)." "qtlimport", help="Import updated i18n files from a zip file."
) )
cmdBuildManual.set_defaults(func=buildPdfManual) cmdImportTS.add_argument("file", help="Path to zip file from Crowdin")
cmdImportTS.set_defaults(func=importI18nUpdates)
# Build Sample
cmdBuildSample = parsers.add_parser(
"sample", help="Build the sample project zip file and add it to assets."
)
cmdBuildSample.set_defaults(func=buildSampleZip)
# Update i18n Sources # Update i18n Sources
cmdUpdateTS = parsers.add_parser( cmdUpdateTS = parsers.add_parser(
@@ -1674,6 +1641,18 @@ if __name__ == "__main__":
) )
cmdBuildQM.set_defaults(func=buildTranslationAssets) 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 # Clean Assets
cmdCleanAssets = parsers.add_parser( cmdCleanAssets = parsers.add_parser(
"clean-assets", help="Delete assets built by manual, sample and qtlrelease." "clean-assets", help="Delete assets built by manual, sample and qtlrelease."
@@ -1686,42 +1665,39 @@ if __name__ == "__main__":
) )
cmdBuildAssets.set_defaults(func=buildAllAssets) 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: # if "gen-plist" in sysArgs:
# sysArgs.remove("gen-plist") # sysArgs.remove("gen-plist")
# genMacOSPlist() # 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: # if "windows-zip" in sysArgs:
# sysArgs.remove("windows-zip") # sysArgs.remove("windows-zip")
# makeWindowsZip() # 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: # if "build-win-exe" in sysArgs:
# sysArgs.remove("build-win-exe") # sysArgs.remove("build-win-exe")
# makeWindowsEmbedded(sysArgs) # makeWindowsEmbedded(sysArgs)