Skip to content

Commit dd7c816

Browse files
committed
Update appimage build
1 parent 2bffa83 commit dd7c816

File tree

6 files changed

+89
-104
lines changed

6 files changed

+89
-104
lines changed

.github/workflows/build_linux.yml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,21 @@ jobs:
1010
needs: buildAssets
1111
runs-on: ubuntu-latest
1212
env:
13-
PYTHON_VERSION: "3.11"
14-
LINUX_TAG: "manylinux_2_28_x86_64"
13+
PYTHON_VERSION: "3.13"
14+
LINUX_TAG: "manylinux_2_28"
15+
LINUX_ARCH: "x86_64"
1516
steps:
1617
- name: Python Setup
1718
uses: actions/setup-python@v5
1819
with:
19-
python-version: "3.11"
20+
python-version: "3.13"
2021
architecture: x64
2122

23+
- name: Install Packages (apt)
24+
run: |
25+
sudo apt update
26+
sudo apt install libxcb-cursor0
27+
2228
- name: Install Packages (pip)
2329
run: pip install python-appimage setuptools
2430

@@ -35,7 +41,7 @@ jobs:
3541
id: build
3642
run: |
3743
echo "BUILD_VERSION=$(python pkgutils.py version)" >> $GITHUB_OUTPUT
38-
python pkgutils.py build-appimage --linux-tag $LINUX_TAG --python-version $PYTHON_VERSION
44+
python pkgutils.py build-appimage $LINUX_TAG $LINUX_ARCH $PYTHON_VERSION
3945
4046
- name: Upload Artifacts
4147
uses: actions/upload-artifact@v4

pkgutils.py

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -272,24 +272,12 @@ def genMacOSPlist(args: argparse.Namespace) -> None:
272272
cmdBuildUbuntu.set_defaults(func=utils.build_debian.launchpad)
273273

274274
# Build AppImage
275-
cmdBuildAppImage = parsers.add_parser(
276-
"build-appimage", help=(
277-
"Build an AppImage. "
278-
"Argument --linux-tag defaults manylinux_2_28_x86_64, and --python-version to 3.13."
279-
)
280-
)
281-
cmdBuildAppImage.add_argument(
282-
"--linux-tag",
283-
default="manylinux_2_28_x86_64",
284-
help=(
285-
"Linux compatibility tag (e.g. manylinux_2_28_x86_64) "
286-
"see https://python-appimage.readthedocs.io/en/latest/#available-python-appimages "
287-
"and https://github.com/pypa/manylinux for a list of valid tags."
288-
),
289-
)
290-
cmdBuildAppImage.add_argument(
291-
"--python-version", default="3.13", help="Python version (e.g. 3.13)"
292-
)
275+
# See https://github.com/pypa/manylinux
276+
# See https://python-appimage.readthedocs.io/en/latest/#available-python-appimages
277+
cmdBuildAppImage = parsers.add_parser("build-appimage", help="Build an AppImage.")
278+
cmdBuildAppImage.add_argument("linux", help="Manylinux version, e.g. manylinux_2_28.")
279+
cmdBuildAppImage.add_argument("arch", help="Architecture, e.g. x86_64.")
280+
cmdBuildAppImage.add_argument("python", help="Python version, e.g. 3.13.")
293281
cmdBuildAppImage.set_defaults(func=utils.build_appimage.appImage)
294282

295283
# Build Windows Inno Setup Installer

setup/icons/novelwriter.png

9.01 KB
Loading
8.64 KB
Loading

utils/build_appimage.py

Lines changed: 44 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@
2222

2323
import argparse
2424
import datetime
25+
import os
2526
import shutil
26-
import subprocess
2727
import sys
2828

29+
from pathlib import Path
30+
2931
from utils.common import (
3032
ROOT_DIR, SETUP_DIR, appdataXml, copyPackageFiles, copySourceCode,
31-
extractVersion, makeCheckSum, toUpload, writeFile
33+
extractVersion, freshFolder, makeCheckSum, systemCall, toUpload, writeFile
3234
)
3335

3436

@@ -49,115 +51,83 @@ def appImage(args: argparse.Namespace) -> None:
4951

5052
print("")
5153
print("Build AppImage")
52-
print("==============")
53-
print("")
54+
print("="*120)
5455

55-
linuxTag = args.linux_tag
56-
pythonVer = args.python_version
56+
mLinux = args.linux
57+
mArch = args.arch
58+
pyVer = args.python
5759

5860
# Version Info
59-
# ============
60-
6161
pkgVers, _, relDate = extractVersion()
6262
relDate = datetime.datetime.strptime(relDate, "%Y-%m-%d")
63-
print("")
6463

6564
# Set Up Folder
66-
# =============
67-
6865
bldDir = ROOT_DIR / "dist_appimage"
69-
bldPkg = f"novelwriter_{pkgVers}"
66+
bldPkg = f"novelwriter-{pkgVers}-{mArch}"
67+
bldImg = f"{bldPkg}.AppImage"
7068
outDir = bldDir / bldPkg
7169
imgDir = bldDir / "appimage"
72-
73-
# Set Up Folders
74-
# ==============
70+
appDir = bldDir / f"novelWriter-{mArch}"
7571

7672
bldDir.mkdir(exist_ok=True)
77-
78-
if outDir.exists():
79-
print("Removing old build files ...")
80-
print("")
81-
shutil.rmtree(outDir)
82-
83-
outDir.mkdir()
84-
85-
if imgDir.exists():
86-
print("Removing old build metadata files ...")
87-
print("")
88-
shutil.rmtree(imgDir)
89-
90-
imgDir.mkdir()
73+
freshFolder(outDir)
74+
freshFolder(imgDir)
75+
freshFolder(appDir)
9176

9277
# Remove old AppImages
9378
if images := bldDir.glob("*.AppImage"):
9479
print("Removing old AppImages")
95-
print("")
9680
for image in images:
9781
image.unlink()
9882

9983
# Copy novelWriter Source
100-
# =======================
101-
10284
print("Copying novelWriter source ...")
103-
print("")
104-
10585
copySourceCode(outDir)
10686

107-
print("")
10887
print("Copying or generating additional files ...")
109-
print("")
110-
11188
copyPackageFiles(outDir)
11289

11390
# Write Metadata
114-
# ==============
115-
11691
writeFile(imgDir / "novelwriter.appdata.xml", appdataXml())
117-
print("Wrote: novelwriter.appdata.xml")
118-
92+
writeFile(imgDir / "requirements.txt", str(outDir))
11993
writeFile(imgDir / "entrypoint.sh", (
120-
'#! /bin/bash \n'
94+
f"export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${{APPDIR}}/usr/lib/{mArch}-linux-gnu/\n"
12195
'{{ python-executable }} -sE ${APPDIR}/opt/python{{ python-version }}/bin/novelwriter "$@"'
12296
))
123-
print("Wrote: entrypoint.sh")
124-
125-
writeFile(imgDir / "requirements.txt", str(outDir))
126-
print("Wrote: requirements.txt")
12797

12898
shutil.copyfile(SETUP_DIR / "data" / "novelwriter.desktop", imgDir / "novelwriter.desktop")
12999
print("Copied: novelwriter.desktop")
130100

131-
shutil.copyfile(SETUP_DIR / "icons" / "novelwriter.svg", imgDir / "novelwriter.svg")
132-
print("Copied: novelwriter.svg")
133-
134-
shutil.copyfile(
135-
SETUP_DIR / "data" / "hicolor" / "256x256" / "apps" / "novelwriter.png",
136-
imgDir / "novelwriter.png"
137-
)
101+
shutil.copyfile(SETUP_DIR / "icons" / "novelwriter.png", imgDir / "novelwriter.png")
138102
print("Copied: novelwriter.png")
139103

140-
# Build AppImage
141-
# ==============
142-
143-
try:
144-
subprocess.call([
145-
sys.executable, "-m", "python_appimage", "build", "app",
146-
"-l", linuxTag, "-p", pythonVer, "appimage"
147-
], cwd=bldDir)
148-
except Exception as exc:
149-
print("AppImage build: FAILED")
150-
print("")
151-
print(str(exc))
152-
print("")
153-
sys.exit(1)
154-
155-
bldFile = list(bldDir.glob("*.AppImage"))[0]
156-
outFile = bldDir / f"novelWriter-{pkgVers}.AppImage"
157-
bldFile.rename(outFile)
158-
shaFile = makeCheckSum(outFile.name, cwd=bldDir)
159-
160-
toUpload(outFile)
104+
# Build AppDir
105+
systemCall([
106+
sys.executable, "-m", "python_appimage", "build", "app", "--no-packaging",
107+
"-l", f"{mLinux}_{mArch}", "-p", pyVer, "appimage"
108+
], cwd=bldDir)
109+
110+
# Copy Libraries
111+
libPath = Path(f"/usr/lib/{mArch}-linux-gnu")
112+
appLibs = appDir / "usr" / "lib" / f"{mArch}-linux-gnu"
113+
appLibs.mkdir(exist_ok=True)
114+
shutil.copyfile(libPath / "libxcb-cursor.so.0", appLibs / "libxcb-cursor.so.0")
115+
116+
# Build Image
117+
env = os.environ.copy()
118+
env["ARCH"] = mArch
119+
systemCall([
120+
"appimagetool", "--no-appstream", "--updateinformation",
121+
f"gh-releases-zsync|vkbo|novelwriter|latest|novelwriter-*-{mArch}.AppImage.zsync",
122+
str(appDir), bldImg
123+
], cwd=bldDir, env=env)
124+
125+
updFile = bldDir / f"{bldImg}.zsync"
126+
bldFile = bldDir / bldImg
127+
shaFile = makeCheckSum(bldFile.name, cwd=bldDir)
128+
129+
toUpload(bldFile)
130+
toUpload(updFile)
161131
toUpload(shaFile)
162132

163133
return

utils/common.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import shutil
2424
import subprocess
25+
import sys
2526

2627
from pathlib import Path
2728

@@ -77,16 +78,16 @@ def copySourceCode(dst: Path) -> None:
7778
for item in src.glob("**/*"):
7879
relSrc = item.relative_to(ROOT_DIR)
7980
if item.suffix in (".pyc", ".pyo"):
80-
print(f"Ignore: {relSrc}")
81+
print("Ignored:", relSrc)
8182
continue
8283
if item.parent.is_dir() and item.parent.name != "__pycache__":
8384
dstDir = dst / relSrc.parent
8485
if not dstDir.exists():
8586
dstDir.mkdir(parents=True)
86-
print(f"Folder: {dstDir}")
87+
print("Created:", dstDir.relative_to(ROOT_DIR))
8788
if item.is_file():
8889
shutil.copyfile(item, dst / relSrc)
89-
print(f"Copied: {dst / relSrc}")
90+
print("Copied:", relSrc)
9091
return
9192

9293

@@ -95,26 +96,23 @@ def copyPackageFiles(dst: Path, setupPy: bool = False) -> None:
9596
copyFiles = ["LICENSE.md", "CREDITS.md", "pyproject.toml"]
9697
for copyFile in copyFiles:
9798
shutil.copyfile(copyFile, dst / copyFile)
98-
print(f"Copied: {copyFile}")
99+
print("Copied:", copyFile)
99100

100101
writeFile(dst / "MANIFEST.in", (
101102
"include LICENSE.md\n"
102103
"include CREDITS.md\n"
103104
"recursive-include novelwriter/assets *\n"
104105
))
105-
print("Wrote: MANIFEST.in")
106106

107107
if setupPy:
108108
writeFile(dst / "setup.py", (
109109
"import setuptools\n"
110110
"setuptools.setup()\n"
111111
))
112-
print("Wrote: setup.py")
113112

114113
text = readFile(ROOT_DIR / "pyproject.toml")
115114
text = text.replace("setup/description_pypi.md", "data/description_short.txt")
116115
writeFile(dst / "pyproject.toml", text)
117-
print("Wrote: pyproject.toml")
118116

119117
return
120118

@@ -188,4 +186,27 @@ def readFile(file: Path) -> str:
188186

189187
def writeFile(file: Path, text: str) -> int:
190188
"""Write string to file."""
191-
return file.write_text(text, encoding="utf-8")
189+
result = file.write_text(text, encoding="utf-8")
190+
print("Wrote:", file.relative_to(ROOT_DIR))
191+
return result
192+
193+
194+
def freshFolder(path: Path) -> None:
195+
"""Make sure a folder exists and is empty."""
196+
if path.exists():
197+
print("Removing:", str(path))
198+
shutil.rmtree(path)
199+
path.mkdir()
200+
return
201+
202+
203+
def systemCall(cmd: list, cwd: Path | str | None = None, env: dict | None = None) -> None:
204+
"""Make a system call using subprocess."""
205+
if isinstance(cwd, Path):
206+
cwd = str(cwd)
207+
try:
208+
subprocess.call([str(c) for c in cmd], cwd=cwd, env=env)
209+
except Exception as exc:
210+
print("ERROR:", str(exc))
211+
sys.exit(1)
212+
return

0 commit comments

Comments
 (0)