Skip to content

Commit a63af34

Browse files
committed
replace bash script with py script
1 parent 76b8334 commit a63af34

3 files changed

Lines changed: 214 additions & 110 deletions

File tree

.github/workflows/appimage.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545
- name: Build AppImage
4646
env:
4747
APPIMAGE_EXTRACT_AND_RUN: "1"
48-
run: bash packaging/linux/appimage/create_appdir.sh
48+
run: ./packaging/linux/appimage/create_appimage.py
4949

5050
- name: Upload AppImage artifact
5151
uses: actions/upload-artifact@v4

packaging/linux/appimage/create_appdir.sh

Lines changed: 0 additions & 109 deletions
This file was deleted.
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
#!/usr/bin/env -S uv run --script
2+
# /// script
3+
# requires-python = ">=3.11"
4+
# dependencies = [
5+
# "plumbum",
6+
# "toml",
7+
# ]
8+
# ///
9+
from __future__ import annotations
10+
11+
import os
12+
import stat
13+
import sys
14+
import urllib.request
15+
from pathlib import Path
16+
17+
import toml
18+
from plumbum import CommandNotFound, local, ProcessExecutionError
19+
from plumbum.cmd import convert as im_convert
20+
from plumbum.cmd import cargo
21+
22+
23+
SIZES = [16, 32, 48, 64, 128, 256]
24+
25+
26+
def die(msg: str, code: int = 1) -> None:
27+
print(msg, file=sys.stderr)
28+
raise SystemExit(code)
29+
30+
31+
def read_version_from_cargo(cargo_toml: Path) -> str | None:
32+
"""Parse version from Cargo.toml."""
33+
if not cargo_toml.is_file():
34+
return None
35+
data = toml.loads(cargo_toml.read_text(encoding="utf-8"))
36+
return data.get("package", {}).get("version")
37+
38+
39+
def ensure_executable(path: Path) -> None:
40+
st = path.stat()
41+
path.chmod(st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
42+
43+
44+
def download_tool(url: str, target: Path) -> None:
45+
"""
46+
Replacement for:
47+
curl -L --fail --retry 3 --retry-delay 2 -o target url
48+
Using urllib.request with basic retry logic.
49+
"""
50+
if target.is_file():
51+
return
52+
53+
print(f"Downloading {target.name} from {url}")
54+
55+
attempts = 3
56+
for attempt in range(1, attempts + 1):
57+
try:
58+
with urllib.request.urlopen(url) as resp, target.open("wb") as out:
59+
out.write(resp.read())
60+
break
61+
except Exception as e:
62+
if attempt == attempts:
63+
die(f"Failed to download {url}: {e}")
64+
else:
65+
print(f"Download failed (attempt {attempt}/{attempts}), retrying…")
66+
import time
67+
68+
time.sleep(2)
69+
70+
ensure_executable(target)
71+
72+
73+
def ensure_newline_at_end(path: Path) -> None:
74+
if not path.is_file():
75+
return
76+
data = path.read_bytes()
77+
if data and not data.endswith(b"\n"):
78+
path.write_bytes(data + b"\n")
79+
80+
81+
def main() -> None:
82+
script_path = Path(__file__).resolve()
83+
script_dir = script_path.parent
84+
root_dir = (script_dir / ".." / ".." / "..").resolve()
85+
86+
build_dir = root_dir / "target"
87+
appimage_dir = build_dir / "appimage"
88+
appdir = appimage_dir / "AppDir"
89+
90+
bin_path = build_dir / "release" / "diskfmt"
91+
desktop_file = script_dir / "diskfmt.desktop"
92+
metadata_file = root_dir / "packaging" / "linux" / "diskfmt.metainfo.xml"
93+
94+
icon_src = Path(os.environ.get("ICON_SRC", root_dir / "assets" / "icon.png"))
95+
arch = os.environ.get("ARCH", os.uname().machine)
96+
97+
version = os.environ.get("VERSION")
98+
if not version:
99+
version = read_version_from_cargo(root_dir / "Cargo.toml")
100+
101+
if not version:
102+
die("Failed to read version from Cargo.toml")
103+
104+
linuxdeploy = Path(
105+
os.environ.get("LINUXDEPLOY", appimage_dir / f"linuxdeploy-{arch}.AppImage")
106+
)
107+
appimagetool = Path(
108+
os.environ.get("APPIMAGETOOL", appimage_dir / f"appimagetool-{arch}.AppImage")
109+
)
110+
111+
linuxdeploy_url = os.environ.get(
112+
"LINUXDEPLOY_URL",
113+
f"https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-{arch}.AppImage",
114+
)
115+
appimagetool_url = os.environ.get(
116+
"APPIMAGETOOL_URL",
117+
f"https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-{arch}.AppImage",
118+
)
119+
120+
os.environ["ARCH"] = arch
121+
os.environ.setdefault("APPIMAGE_EXTRACT_AND_RUN", "1")
122+
123+
# Tool checks
124+
try:
125+
im_convert = local["convert"]
126+
except CommandNotFound:
127+
die("ImageMagick 'convert' is required to generate icons")
128+
129+
if not desktop_file.is_file():
130+
die(f"Desktop file not found: {desktop_file}")
131+
if not icon_src.is_file():
132+
die(f"Icon not found: {icon_src}")
133+
if not metadata_file.is_file():
134+
die(f"Metadata not found: {metadata_file}")
135+
136+
appimage_dir.mkdir(parents=True, exist_ok=True)
137+
138+
# Download dependencies
139+
download_tool(linuxdeploy_url, linuxdeploy)
140+
download_tool(appimagetool_url, appimagetool)
141+
142+
# Build if binary missing
143+
if not (bin_path.is_file() and os.access(bin_path, os.X_OK)):
144+
print(f"Building release binary (missing at {bin_path})")
145+
with local.cwd(root_dir):
146+
cargo["build", "--release"]()
147+
148+
# Clean AppDir
149+
import shutil
150+
151+
shutil.rmtree(appdir, ignore_errors=True)
152+
(appdir / "usr/share/applications").mkdir(parents=True, exist_ok=True)
153+
154+
# Desktop file
155+
desktop_target = appdir / "usr/share/applications/diskfmt.desktop"
156+
shutil.copy2(desktop_file, desktop_target)
157+
158+
if "X-AppImage-Version=" not in desktop_target.read_text():
159+
ensure_newline_at_end(desktop_target)
160+
desktop_target.write_text(
161+
desktop_target.read_text() + f"X-AppImage-Version={version}\n"
162+
)
163+
164+
# Symlink
165+
symlink = appdir / "diskfmt.desktop"
166+
if symlink.exists():
167+
symlink.unlink()
168+
symlink.symlink_to("usr/share/applications/diskfmt.desktop")
169+
170+
# Metadata
171+
metainfo_dir = appdir / "usr/share/metainfo"
172+
metainfo_dir.mkdir(parents=True, exist_ok=True)
173+
shutil.copy2(metadata_file, metainfo_dir / "diskfmt.metainfo.xml")
174+
175+
# Icons
176+
for size in SIZES:
177+
icon_dir = appdir / f"usr/share/icons/hicolor/{size}x{size}/apps"
178+
icon_dir.mkdir(parents=True, exist_ok=True)
179+
im_convert[str(icon_src), "-resize", f"{size}x{size}", str(icon_dir / "diskfmt.png")]()
180+
181+
shutil.copy2(
182+
appdir / "usr/share/icons/hicolor/256x256/apps/diskfmt.png",
183+
appdir / "diskfmt.png",
184+
)
185+
186+
# linuxdeploy
187+
local[linuxdeploy](
188+
"--appdir", str(appdir),
189+
"--executable", str(bin_path),
190+
"--desktop-file", str(desktop_target),
191+
"--icon-file", str(appdir / "diskfmt.png"),
192+
)
193+
194+
output = Path(
195+
os.environ.get(
196+
"OUTPUT", appimage_dir / f"diskfmt-{version}-{arch}.AppImage"
197+
)
198+
)
199+
output.parent.mkdir(parents=True, exist_ok=True)
200+
if output.exists():
201+
output.unlink()
202+
203+
# appimagetool
204+
local[appimagetool](str(appdir), str(output))
205+
206+
print(f"Created {output}")
207+
208+
209+
if __name__ == "__main__":
210+
try:
211+
main()
212+
except ProcessExecutionError as e:
213+
die(f"Command failed ({e.retcode}): {e}")

0 commit comments

Comments
 (0)