feat(release): Packaging für Zorin OS 18 (.deb & .AppImage), In-App Gitea Updater und CI/CD Workflow
Build & Publish Release / Build Packages & Publish to Gitea (push) Successful in 5m26s

This commit is contained in:
harald committed 2026-10-06 11:30:04 +02:00
1 parent 8a25b1e2f3
commit dfb5a64917
17 files changed
+1698 -9

No files matched your search

+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# ==============================================================================
# Haupt-Release-Build-Skript für ink-notes (Zorin OS 18 / Linux)
# Erstellt .deb, .AppImage und SHA256-Prüfsummen in target/
# ==============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${ROOT_DIR}"
VERSION=$(grep -m1 '^version = ' Cargo.toml | cut -d '"' -f2)
DEB_NAME="ink-notes_${VERSION}_amd64.deb"
APPIMAGE_NAME="ink-notes_${VERSION}_x86_64.AppImage"
CHECKSUM_FILE="SHA256SUMS.txt"
echo "======================================================================"
echo "Starte Release-Build für ink-notes v${VERSION}"
echo "======================================================================"
# 1. Debian-Paket bauen (.deb)
"${SCRIPT_DIR}/package-deb.sh"
# 2. AppImage bauen (.AppImage)
"${SCRIPT_DIR}/package-appimage.sh"
# 3. Checksums berechnen
echo "=== Berechne SHA256-Prüfsummen in target/${CHECKSUM_FILE} ==="
cd "${ROOT_DIR}/target"
sha256sum "${DEB_NAME}" "${APPIMAGE_NAME}" > "${CHECKSUM_FILE}"
echo "======================================================================"
echo "Release-Artefakte für v${VERSION} erfolgreich erstellt:"
echo "======================================================================"
ls -lh "${DEB_NAME}" "${APPIMAGE_NAME}" "${CHECKSUM_FILE}"
echo ""
echo "Inhalt von target/${CHECKSUM_FILE}:"
cat "${CHECKSUM_FILE}"
echo "======================================================================"
+192
View File
@@ -0,0 +1,192 @@
#!/usr/bin/env python3
"""
Erzeugt ein 256x256 PNG-Icon für ink-notes mit Python Standard-Bibliothek (ohne externe Module).
"""
import math
import os
import struct
import zlib
WIDTH = 256
HEIGHT = 256
def make_png(width, height, rgba_data):
def chunk(tag, data):
return (
struct.pack(">I", len(data))
+ tag
+ data
+ struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)
)
header = b"\x89PNG\r\n\x1a\n"
ihdr = chunk(tag=b"IHDR", data=struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0))
raw = bytearray()
for y in range(height):
raw.append(0) # Filter type 0 (None)
raw.extend(rgba_data[y * width * 4 : (y + 1) * width * 4])
idat = chunk(tag=b"IDAT", data=zlib.compress(bytes(raw), 9))
iend = chunk(tag=b"IEND", data=b"")
return header + ihdr + idat + iend
def render_icon():
buffer = bytearray(WIDTH * HEIGHT * 4)
def set_pixel(x, y, r, g, b, a):
if 0 <= x < WIDTH and 0 <= y < HEIGHT:
idx = (y * WIDTH + x) * 4
src_a = a / 255.0
dst_a = buffer[idx + 3] / 255.0
out_a = src_a + dst_a * (1.0 - src_a)
if out_a > 1e-5:
out_r = (r * src_a + buffer[idx] * dst_a * (1.0 - src_a)) / out_a
out_g = (
g * src_a + buffer[idx + 1] * dst_a * (1.0 - src_a)
) / out_a
out_b = (
b * src_a + buffer[idx + 2] * dst_a * (1.0 - src_a)
) / out_a
buffer[idx] = int(round(out_r))
buffer[idx + 1] = int(round(out_g))
buffer[idx + 2] = int(round(out_b))
buffer[idx + 3] = int(round(out_a * 255.0))
# 1. Abgerundete Squircle-Kachel (Zorin OS Stil)
cx, cy = WIDTH / 2.0, HEIGHT / 2.0
half_size = 114.0
radius = 52.0
for y in range(HEIGHT):
for x in range(WIDTH):
dx = abs(x - cx)
dy = abs(y - cy)
# Distance from rounded box
qx = max(0.0, dx - (half_size - radius))
qy = max(0.0, dy - (half_size - radius))
dist = math.sqrt(qx * qx + qy * qy)
if dx <= half_size and dy <= half_size and dist <= radius:
# Gradient von Dunkelblau (#1e2638) nach Nachtblau (#0f1422)
t = (x + y) / (WIDTH + HEIGHT)
r = int(30 * (1 - t) + 12 * t)
g = int(38 * (1 - t) + 16 * t)
b = int(58 * (1 - t) + 28 * t)
# Rand-Antialiasing
edge_dist = radius - dist
alpha = min(255, max(0, int(edge_dist * 255)))
# Subtiler Rahmen
if edge_dist < 2.5:
r, g, b = 60, 75, 110
set_pixel(x, y, r, g, b, alpha)
# 2. Subtile Gitterlinien
for gy in range(80, 200, 30):
for gx in range(40, 216):
set_pixel(gx, gy, 140, 160, 190, 25)
for gx in range(70, 200, 30):
for gy in range(50, 216):
set_pixel(gx, gy, 140, 160, 190, 25)
# 3. Tintenspur (Bézier-Kurve mit Glow)
pts = []
for t_i in range(150):
t = t_i / 150.0
# P0=(55, 195), P1=(90, 215), P2=(130, 190), P3=(120, 155), P4=(140, 110), P5=(170, 110)
# 2 Teilkurven
if t <= 0.5:
u = t * 2.0
bx = (
(1 - u) ** 3 * 55
+ 3 * (1 - u) ** 2 * u * 90
+ 3 * (1 - u) * u**2 * 130
+ u**3 * 120
)
by = (
(1 - u) ** 3 * 195
+ 3 * (1 - u) ** 2 * u * 215
+ 3 * (1 - u) * u**2 * 190
+ u**3 * 155
)
else:
u = (t - 0.5) * 2.0
bx = (
(1 - u) ** 3 * 120
+ 3 * (1 - u) ** 2 * u * 110
+ 3 * (1 - u) * u**2 * 140
+ u**3 * 170
)
by = (
(1 - u) ** 3 * 155
+ 3 * (1 - u) ** 2 * u * 120
+ 3 * (1 - u) * u**2 * 95
+ u**3 * 110
)
pts.append((bx, by))
for px, py in pts:
for ox in range(-7, 8):
for oy in range(-7, 8):
d = math.sqrt(ox * ox + oy * oy)
if d <= 7.0:
a = int((1.0 - d / 7.0) * 110)
set_pixel(int(px + ox), int(py + oy), 59, 130, 246, a)
for ox in range(-3, 4):
for oy in range(-3, 4):
d = math.sqrt(ox * ox + oy * oy)
if d <= 3.0:
a = int((1.0 - d / 3.0) * 230)
set_pixel(int(px + ox), int(py + oy), 147, 197, 253, a)
# 4. Füllfederhalter (diagonale Geometrie)
# Stift-Mittelachse von (120, 55) nach (170, 110)
pen_dir = (math.cos(math.radians(45)), math.sin(math.radians(45)))
norm = (-pen_dir[1], pen_dir[0])
# Spitze bei (110, 195)
tip_x, tip_y = 55.0, 195.0
for l in range(0, 120):
pos_x = tip_x + pen_dir[0] * l
pos_y = tip_y - pen_dir[1] * l
# Radius entlang des Stifts
if l < 20:
# Goldene Feder
w = (l / 20.0) * 10.0
color = (245, 158, 11) # Gold
elif l < 32:
# Griffzone
w = 11.0
color = (30, 41, 59) # Dunkelgrau
else:
# Schaft
w = 12.0
color = (59, 130, 246) # Blau
for s in range(int(-w), int(w) + 1):
px = int(pos_x + norm[0] * s)
py = int(pos_y + norm[1] * s)
set_pixel(px, py, color[0], color[1], color[2], 255)
# 5. Glanzpunkt auf der Spitze
for ox in range(-4, 5):
for oy in range(-4, 5):
d = math.sqrt(ox * ox + oy * oy)
if d <= 4:
set_pixel(int(tip_x + ox), int(tip_y + oy), 255, 255, 255, 255)
return make_png(WIDTH, HEIGHT, buffer)
if __name__ == "__main__":
os.makedirs("assets", exist_ok=True)
png_data = render_icon()
with open("assets/ink-notes.png", "wb") as f:
f.write(png_data)
print("PNG-Icon erfolgreich generiert: assets/ink-notes.png (256x256)")
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# ==============================================================================
# Baut ein portables .AppImage-Paket für ink-notes (Zorin OS 18 / Linux x86_64)
# ==============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${ROOT_DIR}"
# 1. Version aus Cargo.toml ermitteln
VERSION=$(grep -m1 '^version = ' Cargo.toml | cut -d '"' -f2)
PKG_NAME="ink-notes"
ARCH="x86_64"
APPIMAGE_NAME="${PKG_NAME}_${VERSION}_${ARCH}.AppImage"
APPDIR="${ROOT_DIR}/target/AppDir"
CACHE_DIR="${ROOT_DIR}/target/tools"
APPIMAGETOOL="${CACHE_DIR}/appimagetool-x86_64.AppImage"
echo "=== [1/5] Stelle Release-Binary sicher (ink-notes v${VERSION}) ==="
if [ ! -f "target/release/ink-notes" ]; then
cargo build --release
fi
# Sicherstellen, dass das PNG-Icon existiert
if [ ! -f "assets/ink-notes.png" ]; then
echo "Generiere PNG-Icon..."
python3 scripts/generate_png_icon.py
fi
echo "=== [2/5] Bereite AppDir-Struktur vor in ${APPDIR} ==="
rm -rf "${APPDIR}"
mkdir -p "${APPDIR}/usr/bin"
mkdir -p "${APPDIR}/usr/share/applications"
mkdir -p "${APPDIR}/usr/share/icons/hicolor/scalable/apps"
mkdir -p "${APPDIR}/usr/share/icons/hicolor/256x256/apps"
mkdir -p "${APPDIR}/usr/share/metainfo"
# Binaries & Assets kopieren
cp "target/release/ink-notes" "${APPDIR}/usr/bin/ink-notes"
chmod 755 "${APPDIR}/usr/bin/ink-notes"
cp "packaging/ink-notes.desktop" "${APPDIR}/usr/share/applications/ink-notes.desktop"
cp "packaging/ink-notes.desktop" "${APPDIR}/ink-notes.desktop"
cp "assets/ink-notes.svg" "${APPDIR}/usr/share/icons/hicolor/scalable/apps/ink-notes.svg"
cp "assets/ink-notes.png" "${APPDIR}/usr/share/icons/hicolor/256x256/apps/ink-notes.png"
cp "assets/ink-notes.png" "${APPDIR}/ink-notes.png"
ln -sf "ink-notes.png" "${APPDIR}/.DirIcon"
cp "packaging/ink-notes.metainfo.xml" "${APPDIR}/usr/share/metainfo/eu.pansi.ink-notes.metainfo.xml"
# AppRun Skript erstellen
cat <<'EOF' > "${APPDIR}/AppRun"
#!/usr/bin/env bash
SELF_DIR="$(dirname "$(readlink -f "$0")")"
export PATH="${SELF_DIR}/usr/bin:${PATH}"
export XDG_DATA_DIRS="${SELF_DIR}/usr/share:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
export APPDIR="${SELF_DIR}"
exec "${SELF_DIR}/usr/bin/ink-notes" "$@"
EOF
chmod 755 "${APPDIR}/AppRun"
echo "=== [3/5] Hole appimagetool (gecacht in ${CACHE_DIR}) ==="
mkdir -p "${CACHE_DIR}"
if [ ! -f "${APPIMAGETOOL}" ]; then
echo "Lade appimagetool herunter..."
curl -fsSL -o "${APPIMAGETOOL}" "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod +x "${APPIMAGETOOL}"
fi
echo "=== [4/5] Erstelle AppImage mit appimagetool ==="
# --appimage-extract-and-run erlaubt Ausführung auch ohne root / FUSE
# NO_APPSTREAM=1 verhindert Build-Abbruch bei privaten/nicht-erreichbaren URLs im Metainfo-XML
export NO_APPSTREAM=1
ARCH="${ARCH}" "${APPIMAGETOOL}" --appimage-extract-and-run "${APPDIR}" "${ROOT_DIR}/target/${APPIMAGE_NAME}"
echo "=== [5/5] AppImage erfolgreich erstellt! ==="
ls -lh "${ROOT_DIR}/target/${APPIMAGE_NAME}"
echo "Pfad: ${ROOT_DIR}/target/${APPIMAGE_NAME}"
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
# ==============================================================================
# Baut ein natives .deb-Paket für Zorin OS 18 / Ubuntu / Debian
# ==============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${ROOT_DIR}"
# 1. Version aus Cargo.toml ermitteln
VERSION=$(grep -m1 '^version = ' Cargo.toml | cut -d '"' -f2)
PKG_NAME="ink-notes"
ARCH="amd64"
DEB_NAME="${PKG_NAME}_${VERSION}_${ARCH}.deb"
STAGING_DIR="${ROOT_DIR}/target/deb_staging"
echo "=== [1/4] Baue Release-Binary mit Cargo (ink-notes v${VERSION}) ==="
cargo build --release
# Sicherstellen, dass das PNG-Icon existiert
if [ ! -f "assets/ink-notes.png" ]; then
echo "Generiere PNG-Icon..."
python3 scripts/generate_png_icon.py
fi
echo "=== [2/4] Erstelle Paketstruktur in ${STAGING_DIR} ==="
rm -rf "${STAGING_DIR}"
mkdir -p "${STAGING_DIR}/DEBIAN"
mkdir -p "${STAGING_DIR}/usr/bin"
mkdir -p "${STAGING_DIR}/usr/share/applications"
mkdir -p "${STAGING_DIR}/usr/share/icons/hicolor/scalable/apps"
mkdir -p "${STAGING_DIR}/usr/share/icons/hicolor/256x256/apps"
mkdir -p "${STAGING_DIR}/usr/share/metainfo"
# 2. Dateien kopieren
cp "target/release/ink-notes" "${STAGING_DIR}/usr/bin/ink-notes"
chmod 755 "${STAGING_DIR}/usr/bin/ink-notes"
cp "packaging/ink-notes.desktop" "${STAGING_DIR}/usr/share/applications/ink-notes.desktop"
chmod 644 "${STAGING_DIR}/usr/share/applications/ink-notes.desktop"
cp "assets/ink-notes.svg" "${STAGING_DIR}/usr/share/icons/hicolor/scalable/apps/ink-notes.svg"
chmod 644 "${STAGING_DIR}/usr/share/icons/hicolor/scalable/apps/ink-notes.svg"
cp "assets/ink-notes.png" "${STAGING_DIR}/usr/share/icons/hicolor/256x256/apps/ink-notes.png"
chmod 644 "${STAGING_DIR}/usr/share/icons/hicolor/256x256/apps/ink-notes.png"
cp "packaging/ink-notes.metainfo.xml" "${STAGING_DIR}/usr/share/metainfo/eu.pansi.ink-notes.metainfo.xml"
chmod 644 "${STAGING_DIR}/usr/share/metainfo/eu.pansi.ink-notes.metainfo.xml"
# 3. DEBIAN/control Datei generieren
cat <<EOF > "${STAGING_DIR}/DEBIAN/control"
Package: ${PKG_NAME}
Version: ${VERSION}
Architecture: ${ARCH}
Maintainer: Harald <harald@pansi.eu>
Installed-Size: $(du -ks "${STAGING_DIR}/usr" | cut -f1)
Depends: libc6, libx11-6, libxcursor1, libxrandr2, libxi6, libxkbcommon0
Section: utils
Priority: optional
Homepage: https://farmersdog.pansi.eu/harald/ink-notes
Description: Latenzoptimierte Vektor-Notizbuch- und Inking-App für Linux
ink-notes kombiniert minimale Stylus-Latenz mit GPU-Hardwarebeschleunigung (wgpu),
stufenlosem Viewport, Geometrie- und Formerkennung, Objekt-Transformationen,
Multi-Dokumenten-Verwaltung und Vektor-PDF-Export.
EOF
echo "=== [3/4] Schnüre .deb-Paket mit dpkg-deb ==="
dpkg-deb --build --root-owner-group "${STAGING_DIR}" "${ROOT_DIR}/target/${DEB_NAME}"
echo "=== [4/4] Paket erfolgreich erstellt: target/${DEB_NAME} ==="
ls -lh "${ROOT_DIR}/target/${DEB_NAME}"
dpkg-deb -I "${ROOT_DIR}/target/${DEB_NAME}"
echo "Fertig!"
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env bash
# ==============================================================================
# Veröffentlicht ein Release auf dem Gitea-Server (gitea.pansi.eu)
# Lädt .deb, .AppImage und SHA256SUMS.txt als Release-Assets hoch.
# ==============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${ROOT_DIR}"
GITEA_SERVER="${GITEA_SERVER:-https://gitea.pansi.eu}"
REPO_OWNER="${REPO_OWNER:-harald}"
REPO_NAME="${REPO_NAME:-ink-notes}"
# Token aus Argument oder Umgebungsvariablen ermitteln
TOKEN="${1:-${INK_NOTES_GITEA_TOKEN:-${GITEA_TOKEN:-${GITHUB_TOKEN:-}}}}"
if [ -z "${TOKEN}" ]; then
echo "FEHLER: Kein Gitea-API-Token angegeben."
echo "Verwendung: $0 <GITEA_API_TOKEN>"
echo "Oder setze die Umgebungsvariable INK_NOTES_GITEA_TOKEN."
exit 1
fi
VERSION=$(grep -m1 '^version = ' Cargo.toml | cut -d '"' -f2)
TAG_NAME="v${VERSION}"
RELEASE_TITLE="ink-notes ${TAG_NAME}"
DEB_FILE="${ROOT_DIR}/target/ink-notes_${VERSION}_amd64.deb"
APPIMAGE_FILE="${ROOT_DIR}/target/ink-notes_${VERSION}_x86_64.AppImage"
CHECKSUMS_FILE="${ROOT_DIR}/target/SHA256SUMS.txt"
# Sicherstellen, dass die Artefakte existieren
if [ ! -f "${DEB_FILE}" ] || [ ! -f "${APPIMAGE_FILE}" ] || [ ! -f "${CHECKSUMS_FILE}" ]; then
echo "Artefakte nicht vollständig gefunden. Führe build-release.sh aus..."
"${SCRIPT_DIR}/build-release.sh"
fi
echo "======================================================================"
echo "Veröffentliche Release ${TAG_NAME} auf ${GITEA_SERVER}/${REPO_OWNER}/${REPO_NAME}"
echo "======================================================================"
RELEASE_BODY=$(cat <<EOF
## ink-notes ${TAG_NAME}
Latenzoptimierte Vektor-Notizbuch- und Inking-Anwendung für Linux (Zorin OS 18 / Ubuntu 24.04).
### Highlights:
- **Zwei-Stufen-Inking-Modell**: Kausale Echtzeit-Glättung via 1€-Filter & akausale Ecken-Erhaltung (Corner Preservation)
- **Geometrie- und Formerkennung**: Geraden, Rechtecke, Kreise, Rauten, Polygone & Koordinatensysteme (Draw-and-Hold)
- **Viewport**: Stufenloser Zoom (25% - 500%) und Panning
- **Multi-Dokumenten-Manager**: Unbegrenzte Notizbücher mit atomarem Autosave
- **In-App Updater**: Automatische Versionsprüfung und Aktualisierung via Gitea Releases
- **Vektor-PDF-Export**: Verluste freie Ausgabe für Druck und Weitergabe
### Installation:
- **Zorin OS / Ubuntu / Debian (.deb)**:
\`\`\`bash
sudo dpkg -i ink-notes_${VERSION}_amd64.deb
\`\`\`
- **Portables AppImage (Alle Linux-Distributionen)**:
\`\`\`bash
chmod +x ink-notes_${VERSION}_x86_64.AppImage
./ink-notes_${VERSION}_x86_64.AppImage
\`\`\`
EOF
)
# 1. Release via Gitea REST API erstellen (oder ID abfragen, falls bereits existent)
CREATE_PAYLOAD=$(python3 -c "
import json, sys
data = {
'tag_name': '${TAG_NAME}',
'target_commitish': 'main',
'name': '${RELEASE_TITLE}',
'body': '''${RELEASE_BODY}''',
'draft': False,
'prerelease': False
}
print(json.dumps(data))
")
API_BASE="${GITEA_SERVER}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}"
echo "Erstelle Release über Gitea API..."
HTTP_RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \
-X POST "${API_BASE}/releases" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Authorization: token ${TOKEN}" \
-d "${CREATE_PAYLOAD}")
HTTP_STATUS=$(echo "${HTTP_RESPONSE}" | grep "HTTP_STATUS:" | cut -d':' -f2)
RESPONSE_BODY=$(echo "${HTTP_RESPONSE}" | sed '/HTTP_STATUS:/d')
RELEASE_ID=""
if [ "${HTTP_STATUS}" -eq 201 ]; then
RELEASE_ID=$(echo "${RESPONSE_BODY}" | python3 -c "import sys, json; print(json.load(sys.stdin)['id'])")
echo "Release erfolgreich angelegt (ID: ${RELEASE_ID})!"
else
echo "Release existiert womöglich bereits (Status ${HTTP_STATUS}). Suche bestehendes Release..."
EXISTING_RELEASE=$(curl -fsSL \
-H "Accept: application/json" \
-H "Authorization: token ${TOKEN}" \
"${API_BASE}/releases/tags/${TAG_NAME}" || true)
if [ -n "${EXISTING_RELEASE}" ]; then
RELEASE_ID=$(echo "${EXISTING_RELEASE}" | python3 -c "import sys, json; print(json.load(sys.stdin).get('id', ''))")
fi
fi
if [ -z "${RELEASE_ID}" ]; then
echo "FEHLER: Konnte Release nicht erstellen oder abfragen. Serverantwort:"
echo "${RESPONSE_BODY}"
exit 1
fi
# 2. Assets hochladen
upload_asset() {
local file_path="$1"
local filename="$(basename "${file_path}")"
echo "Lade Asset hoch: ${filename}..."
curl -fsSL \
-X POST "${API_BASE}/releases/${RELEASE_ID}/assets?name=${filename}" \
-H "Accept: application/json" \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary "@${file_path}" > /dev/null
echo "✓ ${filename} erfolgreich hochgeladen."
}
upload_asset "${DEB_FILE}"
upload_asset "${APPIMAGE_FILE}"
upload_asset "${CHECKSUMS_FILE}"
echo "======================================================================"
echo "Release ${TAG_NAME} erfolgreich veröffentlicht!"
echo "URL: ${GITEA_SERVER}/${REPO_OWNER}/${REPO_NAME}/releases/tag/${TAG_NAME}"
echo "======================================================================"