v2.2.2
This commit is contained in:
parent
f3e39fb8e6
commit
e5f5d32aab
124 changed files with 11892 additions and 2461 deletions
53
packaging/build-appimage.sh
Executable file
53
packaging/build-appimage.sh
Executable file
|
|
@ -0,0 +1,53 @@
|
|||
#!/bin/bash
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
# Build zupt as AppImage (portable single-file binary).
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
VERSION="${VERSION:-2.2.2}"
|
||||
ARCH="${ARCH:-x86_64}"
|
||||
NAME="zupt-$VERSION-$ARCH"
|
||||
OUT="/tmp/${NAME}.AppDir"
|
||||
|
||||
rm -rf "$OUT"
|
||||
mkdir -p "$OUT/usr/bin" "$OUT/usr/lib" "$OUT/usr/share/applications" "$OUT/usr/share/icons/hicolor/256x256/apps"
|
||||
|
||||
install -m 755 zupt "$OUT/usr/bin/"
|
||||
install -m 644 vendor/zuptsdk/libzuptsdk.so.2.0.0 "$OUT/usr/lib/"
|
||||
ln -sf libzuptsdk.so.2.0.0 "$OUT/usr/lib/libzuptsdk.so.2"
|
||||
ln -sf libzuptsdk.so.2 "$OUT/usr/lib/libzuptsdk.so"
|
||||
|
||||
cat > "$OUT/AppRun" <<'APPRUN'
|
||||
#!/bin/bash
|
||||
HERE="$(dirname "$(readlink -f "${0}")")"
|
||||
export LD_LIBRARY_PATH="$HERE/usr/lib:$LD_LIBRARY_PATH"
|
||||
export PATH="$HERE/usr/bin:$PATH"
|
||||
exec "$HERE/usr/bin/zupt" "$@"
|
||||
APPRUN
|
||||
chmod +x "$OUT/AppRun"
|
||||
|
||||
cat > "$OUT/zupt.desktop" <<'DESK'
|
||||
[Desktop Entry]
|
||||
Name=Zupt
|
||||
Comment=Post-quantum backup compression utility
|
||||
Exec=zupt
|
||||
Terminal=true
|
||||
Type=Application
|
||||
Categories=Utility;Archiving;
|
||||
Icon=zupt
|
||||
DESK
|
||||
cp "$OUT/zupt.desktop" "$OUT/usr/share/applications/"
|
||||
|
||||
printf '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\rIDATx\x9cc\xfc\xcf\xc0\x00\x00\x00\x05\x00\x01\xa5\xf6E@\x00\x00\x00\x00IEND\xaeB`\x82' > "$OUT/zupt.png"
|
||||
cp "$OUT/zupt.png" "$OUT/usr/share/icons/hicolor/256x256/apps/zupt.png"
|
||||
|
||||
if command -v appimagetool >/dev/null 2>&1; then
|
||||
ARCH=$ARCH appimagetool "$OUT" "/tmp/${NAME}.AppImage"
|
||||
echo "Built: /tmp/${NAME}.AppImage"
|
||||
else
|
||||
cd /tmp
|
||||
tar -czf "${NAME}.AppDir.tar.gz" "$(basename "$OUT")"
|
||||
echo "appimagetool not present; built AppDir tarball: /tmp/${NAME}.AppDir.tar.gz"
|
||||
echo "Users can run: tar xzf ${NAME}.AppDir.tar.gz && ./${NAME}.AppDir/AppRun"
|
||||
fi
|
||||
133
packaging/build-deb.sh
Executable file
133
packaging/build-deb.sh
Executable file
|
|
@ -0,0 +1,133 @@
|
|||
#!/bin/bash
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
# Build zupt CLI .deb package
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
VERSION="${VERSION:-2.2.2}"
|
||||
ARCH="${ARCH:-amd64}"
|
||||
|
||||
# Multi-arch lib path mapping (Debian convention)
|
||||
case "$ARCH" in
|
||||
amd64) MULTIARCH="x86_64-linux-gnu" ;;
|
||||
arm64) MULTIARCH="aarch64-linux-gnu" ;;
|
||||
armhf) MULTIARCH="arm-linux-gnueabihf" ;;
|
||||
i386) MULTIARCH="i386-linux-gnu" ;;
|
||||
*) MULTIARCH="$ARCH-linux-gnu" ;;
|
||||
esac
|
||||
|
||||
PKG="zupt_${VERSION}_${ARCH}"
|
||||
ROOT="/tmp/$PKG"
|
||||
|
||||
rm -rf "$ROOT"
|
||||
mkdir -p "$ROOT/DEBIAN" \
|
||||
"$ROOT/usr/bin" \
|
||||
"$ROOT/usr/share/doc/zupt" \
|
||||
"$ROOT/usr/share/man/man1"
|
||||
|
||||
# Binary
|
||||
install -m 755 zupt "$ROOT/usr/bin/zupt"
|
||||
|
||||
# Note: libzuptsdk.so.2 is provided by the libzuptsdk2 package (separate
|
||||
# project). We do NOT bundle it. The user must install libzuptsdk2 to
|
||||
# satisfy the Depends: line below, which gives them the SDK on its own
|
||||
# upgrade cycle.
|
||||
|
||||
# Docs
|
||||
install -m 644 README.md CHANGELOG.md SECURITY.md AUDIT.md "$ROOT/usr/share/doc/zupt/"
|
||||
gzip -9n -c CHANGELOG.md > "$ROOT/usr/share/doc/zupt/changelog.gz"
|
||||
|
||||
# Man page (use the real one from doc/zupt.1)
|
||||
if [ -f doc/zupt.1 ]; then
|
||||
install -m 644 doc/zupt.1 "$ROOT/usr/share/man/man1/zupt.1"
|
||||
else
|
||||
cat > "$ROOT/usr/share/man/man1/zupt.1" <<'MAN'
|
||||
.TH ZUPT 1 "April 2026" "zupt 2.2.2" "User Commands"
|
||||
.SH NAME
|
||||
zupt \- post-quantum backup compression utility
|
||||
.SH SYNOPSIS
|
||||
.B zupt
|
||||
[\fIcommand\fR] [\fIoptions\fR] \fIarchive\fR [\fIfiles...\fR]
|
||||
.SH SEE ALSO
|
||||
Run \fBzupt help\fR for the full options reference.
|
||||
MAN
|
||||
fi
|
||||
gzip -9n "$ROOT/usr/share/man/man1/zupt.1"
|
||||
|
||||
# Copyright
|
||||
cat > "$ROOT/usr/share/doc/zupt/copyright" <<'COPYRIGHT'
|
||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: zupt
|
||||
Upstream-Contact: Cristian Cezar Moisés <zupt@riseup.net>
|
||||
Source: https://git.securityops.co/cristiancmoises/zupt
|
||||
|
||||
Files: *
|
||||
Copyright: 2025-2026 Cristian Cezar Moisés
|
||||
License: AGPL-3.0+
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
.
|
||||
On Debian systems, the complete text of the GNU Affero General Public
|
||||
License version 3 can be found in /usr/share/common-licenses/AGPL-3.
|
||||
|
||||
Files: src/vv_*.c src/vaptvupt_api.c include/vv_*.h include/vaptvupt*.h
|
||||
Copyright: 2025-2026 Cristian Cezar Moisés
|
||||
License: GPL-3.0+
|
||||
The VaptVupt LZ codec is licensed under the GNU General Public License
|
||||
version 3 or later (NOT AGPL like the rest of the project). VaptVupt
|
||||
is GPL so that, with sufficient maturity, it can be considered for
|
||||
upstreaming into the Linux or BSD kernels.
|
||||
.
|
||||
On Debian systems, the complete text of the GNU General Public
|
||||
License version 3 can be found in /usr/share/common-licenses/GPL-3.
|
||||
|
||||
Comment:
|
||||
Commercial licenses (relief from AGPL/GPL copyleft terms) are
|
||||
available for both components. Contact sac@securityops.co for
|
||||
commercial inquiries.
|
||||
COPYRIGHT
|
||||
|
||||
# Control
|
||||
INSTALLED_SIZE=$(du -sk "$ROOT" | cut -f1)
|
||||
cat > "$ROOT/DEBIAN/control" <<EOF
|
||||
Package: zupt
|
||||
Version: $VERSION
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Architecture: $ARCH
|
||||
Depends: libc6 (>= 2.28), libargon2-1, libssl3, libzuptsdk2 (>= 2.0.0)
|
||||
Maintainer: Cristian Cezar Moisés <zupt@riseup.net>
|
||||
Installed-Size: $INSTALLED_SIZE
|
||||
Homepage: https://git.securityops.co/cristiancmoises/zupt
|
||||
Description: Post-quantum backup compression utility
|
||||
Zupt is a backup-oriented compression utility with hybrid post-quantum
|
||||
encryption (ML-KEM-768 + X25519). It provides authenticated encryption,
|
||||
multi-threaded compression, full-disk backup/restore, and integrates
|
||||
libzuptsdk for state-of-the-art crypto (HKDF combiner, key commitment,
|
||||
HPKE binding, anti-fault decapsulation).
|
||||
EOF
|
||||
|
||||
# Postinst: ldconfig
|
||||
cat > "$ROOT/DEBIAN/postinst" <<'POSTINST'
|
||||
#!/bin/sh
|
||||
set -e
|
||||
ldconfig
|
||||
POSTINST
|
||||
chmod 755 "$ROOT/DEBIAN/postinst"
|
||||
|
||||
cat > "$ROOT/DEBIAN/postrm" <<'POSTRM'
|
||||
#!/bin/sh
|
||||
set -e
|
||||
if [ "$1" = "remove" ] || [ "$1" = "purge" ]; then
|
||||
ldconfig
|
||||
fi
|
||||
POSTRM
|
||||
chmod 755 "$ROOT/DEBIAN/postrm"
|
||||
|
||||
# Build
|
||||
dpkg-deb -Zxz --build --root-owner-group "$ROOT" "/tmp/$PKG.deb"
|
||||
echo "Built: /tmp/$PKG.deb"
|
||||
dpkg-deb --info "/tmp/$PKG.deb" | head -15
|
||||
121
packaging/build-gui-appimage.sh
Executable file
121
packaging/build-gui-appimage.sh
Executable file
|
|
@ -0,0 +1,121 @@
|
|||
#!/bin/bash
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
# Build zupt-gui AppImage. Since the GUI is pure Python + Qt, the AppDir
|
||||
# bundles only the Python source and metadata; it relies on system
|
||||
# python3 + PyQt6/PySide6 at runtime. This keeps the AppImage tiny
|
||||
# (~50 KB) and lets it work on any Linux with Qt6 Python bindings.
|
||||
#
|
||||
# For a true self-contained AppImage with bundled Python interpreter,
|
||||
# use python-appimage (https://github.com/niess/python-appimage) on
|
||||
# the build host — it produces a ~80 MB AppImage. The portable variant
|
||||
# below is the better tradeoff for most distributions.
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
VERSION="${VERSION:-1.1.1}"
|
||||
APPDIR="/tmp/zupt-gui.AppDir"
|
||||
|
||||
rm -rf "$APPDIR"
|
||||
mkdir -p "$APPDIR/usr/bin" \
|
||||
"$APPDIR/usr/lib/zupt-gui" \
|
||||
"$APPDIR/usr/share/applications" \
|
||||
"$APPDIR/usr/share/icons/hicolor/256x256/apps"
|
||||
|
||||
# Python source
|
||||
install -m 644 gui/src/zupt_gui.py "$APPDIR/usr/lib/zupt-gui/"
|
||||
|
||||
# Wrapper
|
||||
cat > "$APPDIR/usr/bin/zupt-gui" <<'WRAP'
|
||||
#!/bin/sh
|
||||
exec python3 "$(dirname "$0")/../lib/zupt-gui/zupt_gui.py" "$@"
|
||||
WRAP
|
||||
chmod 755 "$APPDIR/usr/bin/zupt-gui"
|
||||
|
||||
# Desktop file
|
||||
cat > "$APPDIR/zupt-gui.desktop" <<'DESKTOP'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Zupt GUI
|
||||
GenericName=Backup and Compression Utility
|
||||
Comment=Post-quantum backup with HKDF combiner, key commitment, HPKE binding
|
||||
Exec=zupt-gui %f
|
||||
Icon=zupt-gui
|
||||
Terminal=false
|
||||
Categories=Utility;Archiving;Compression;Security;
|
||||
StartupNotify=true
|
||||
DESKTOP
|
||||
cp "$APPDIR/zupt-gui.desktop" "$APPDIR/usr/share/applications/"
|
||||
|
||||
# Icon
|
||||
if [ -f gui/assets/zupt-icon.png ]; then
|
||||
cp gui/assets/zupt-icon.png "$APPDIR/zupt-gui.png"
|
||||
cp gui/assets/zupt-icon.png "$APPDIR/usr/share/icons/hicolor/256x256/apps/zupt-gui.png"
|
||||
else
|
||||
python3 -c "
|
||||
import struct, zlib
|
||||
def png(w, h, color):
|
||||
raw = b''.join(b'\\0' + bytes(color) * w for _ in range(h))
|
||||
def chunk(t, d): return struct.pack('>I', len(d)) + t + d + struct.pack('>I', zlib.crc32(t+d) & 0xffffffff)
|
||||
return b'\\x89PNG\\r\\n\\x1a\\n' + chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 0)) + chunk(b'IDAT', zlib.compress(raw)) + chunk(b'IEND', b'')
|
||||
open('$APPDIR/zupt-gui.png','wb').write(png(256, 256, (88, 92, 215)))
|
||||
"
|
||||
cp "$APPDIR/zupt-gui.png" "$APPDIR/usr/share/icons/hicolor/256x256/apps/zupt-gui.png"
|
||||
fi
|
||||
|
||||
# AppRun — sets PATH so zupt-gui finds the bundled wrapper, falls
|
||||
# back to system zupt CLI if not present in /usr/bin alongside.
|
||||
cat > "$APPDIR/AppRun" <<'APPRUN'
|
||||
#!/bin/sh
|
||||
HERE="$(dirname "$(readlink -f "$0")")"
|
||||
export PATH="$HERE/usr/bin:$PATH"
|
||||
|
||||
# Pre-flight check: is python3 available? Is a Qt6 binding installed?
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
cat >&2 <<EOF
|
||||
zupt-gui: python3 is not installed.
|
||||
Install: sudo apt install python3 (Debian/Ubuntu)
|
||||
sudo dnf install python3 (Fedora/RHEL)
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! python3 -c 'import PyQt6.QtWidgets' 2>/dev/null \
|
||||
&& ! python3 -c 'import PySide6.QtWidgets' 2>/dev/null; then
|
||||
cat >&2 <<EOF
|
||||
zupt-gui: needs a Qt6 Python binding (PyQt6 or PySide6).
|
||||
Install one of:
|
||||
Debian/Ubuntu: sudo apt install python3-pyqt6
|
||||
Fedora/RHEL: sudo dnf install python3-pyqt6
|
||||
pip (any): pip install --user PySide6
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v zupt >/dev/null 2>&1; then
|
||||
cat >&2 <<EOF
|
||||
zupt-gui: warning — the 'zupt' CLI was not found in PATH.
|
||||
Install the zupt package or place the binary in PATH.
|
||||
The GUI will start but compress/extract operations will fail.
|
||||
EOF
|
||||
fi
|
||||
|
||||
exec "$HERE/usr/bin/zupt-gui" "$@"
|
||||
APPRUN
|
||||
chmod 755 "$APPDIR/AppRun"
|
||||
|
||||
# Build AppImage
|
||||
if command -v appimagetool >/dev/null 2>&1; then
|
||||
ARCH=x86_64 appimagetool "$APPDIR" "/tmp/Zupt-GUI-$VERSION-x86_64.AppImage" 2>&1 | tail -5
|
||||
echo "Built: /tmp/Zupt-GUI-$VERSION-x86_64.AppImage"
|
||||
else
|
||||
cd /tmp
|
||||
rm -f "Zupt-GUI-$VERSION-x86_64.AppDir.tar.gz"
|
||||
tar -czf "Zupt-GUI-$VERSION-x86_64.AppDir.tar.gz" zupt-gui.AppDir
|
||||
cd - >/dev/null
|
||||
echo "appimagetool unavailable; portable AppDir tarball at:"
|
||||
echo " /tmp/Zupt-GUI-$VERSION-x86_64.AppDir.tar.gz"
|
||||
echo "Run via: tar -xzf ... && ./zupt-gui.AppDir/AppRun"
|
||||
echo "Convert to AppImage on a host with appimagetool:"
|
||||
echo " ARCH=x86_64 appimagetool zupt-gui.AppDir Zupt-GUI-$VERSION-x86_64.AppImage"
|
||||
fi
|
||||
176
packaging/build-gui-deb.sh
Executable file
176
packaging/build-gui-deb.sh
Executable file
|
|
@ -0,0 +1,176 @@
|
|||
#!/bin/bash
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
# Build zupt-gui .deb (Python/Qt GUI). Works with PyQt6 OR PySide6.
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
VERSION="${VERSION:-1.1.1}"
|
||||
ARCH="all"
|
||||
PKG="zupt-gui_${VERSION}_${ARCH}"
|
||||
ROOT="/tmp/$PKG"
|
||||
|
||||
rm -rf "$ROOT"
|
||||
mkdir -p "$ROOT/DEBIAN" \
|
||||
"$ROOT/usr/bin" \
|
||||
"$ROOT/usr/lib/zupt-gui" \
|
||||
"$ROOT/usr/share/applications" \
|
||||
"$ROOT/usr/share/icons/hicolor/256x256/apps" \
|
||||
"$ROOT/usr/share/man/man1" \
|
||||
"$ROOT/usr/share/doc/zupt-gui"
|
||||
|
||||
# Source files
|
||||
install -m 644 gui/src/zupt_gui.py "$ROOT/usr/lib/zupt-gui/"
|
||||
|
||||
# Wrapper script in /usr/bin
|
||||
cat > "$ROOT/usr/bin/zupt-gui" <<'WRAP'
|
||||
#!/bin/sh
|
||||
exec python3 /usr/lib/zupt-gui/zupt_gui.py "$@"
|
||||
WRAP
|
||||
chmod 755 "$ROOT/usr/bin/zupt-gui"
|
||||
|
||||
# Desktop entry
|
||||
cat > "$ROOT/usr/share/applications/zupt-gui.desktop" <<'DESKTOP'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Zupt GUI
|
||||
GenericName=Backup and Compression Utility
|
||||
Comment=Post-quantum backup with HKDF combiner, key commitment, HPKE binding
|
||||
Exec=zupt-gui %f
|
||||
Icon=zupt-gui
|
||||
Terminal=false
|
||||
Categories=Utility;Archiving;Compression;Security;
|
||||
StartupNotify=true
|
||||
MimeType=application/x-zupt;
|
||||
Keywords=archive;compression;encryption;post-quantum;backup;
|
||||
DESKTOP
|
||||
|
||||
# Man page
|
||||
if [ -f doc/zupt-gui.1 ]; then
|
||||
install -m 644 doc/zupt-gui.1 "$ROOT/usr/share/man/man1/zupt-gui.1"
|
||||
gzip -9n "$ROOT/usr/share/man/man1/zupt-gui.1"
|
||||
fi
|
||||
|
||||
# Icon
|
||||
if [ -f gui/assets/zupt-icon.png ]; then
|
||||
cp gui/assets/zupt-icon.png "$ROOT/usr/share/icons/hicolor/256x256/apps/zupt-gui.png"
|
||||
else
|
||||
python3 -c "
|
||||
import struct, zlib
|
||||
def png(w, h, color):
|
||||
raw = b''.join(b'\\0' + bytes(color) * w for _ in range(h))
|
||||
def chunk(t, d): return struct.pack('>I', len(d)) + t + d + struct.pack('>I', zlib.crc32(t+d) & 0xffffffff)
|
||||
return b'\\x89PNG\\r\\n\\x1a\\n' + chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 0)) + chunk(b'IDAT', zlib.compress(raw)) + chunk(b'IEND', b'')
|
||||
open('$ROOT/usr/share/icons/hicolor/256x256/apps/zupt-gui.png','wb').write(png(256, 256, (88, 92, 215)))
|
||||
"
|
||||
fi
|
||||
|
||||
# Docs
|
||||
install -m 644 gui/README.md "$ROOT/usr/share/doc/zupt-gui/" 2>/dev/null || true
|
||||
gzip -9n -c CHANGELOG.md > "$ROOT/usr/share/doc/zupt-gui/changelog.gz"
|
||||
|
||||
cat > "$ROOT/usr/share/doc/zupt-gui/copyright" <<'COPYRIGHT'
|
||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: zupt-gui
|
||||
Upstream-Contact: Cristian Cezar Moisés <zupt@riseup.net>
|
||||
Source: https://git.securityops.co/cristiancmoises/zupt
|
||||
|
||||
Files: *
|
||||
Copyright: 2025-2026 Cristian Cezar Moisés
|
||||
License: AGPL-3.0+
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
.
|
||||
On Debian systems, the complete text of the GNU Affero General Public
|
||||
License version 3 can be found in /usr/share/common-licenses/AGPL-3.
|
||||
COPYRIGHT
|
||||
|
||||
# Control
|
||||
INSTALLED_SIZE=$(du -sk "$ROOT" | cut -f1)
|
||||
cat > "$ROOT/DEBIAN/control" <<EOF
|
||||
Package: zupt-gui
|
||||
Version: $VERSION
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Architecture: $ARCH
|
||||
Depends: python3 (>= 3.9), python3-pyqt6 | python3-pyside6, zupt (>= 2.2.2)
|
||||
Maintainer: Cristian Cezar Moisés <zupt@riseup.net>
|
||||
Installed-Size: $INSTALLED_SIZE
|
||||
Homepage: https://git.securityops.co/cristiancmoises/zupt
|
||||
Description: Graphical interface for the Zupt post-quantum backup utility
|
||||
PySide6/PyQt6 frontend for Zupt. Supports compression, extraction, key
|
||||
management, and full disk backup/restore. Exposes both legacy --pq
|
||||
and new --pq-sdk (libzuptsdk: HKDF combiner, key commitment, HPKE
|
||||
binding, Argon2id) encryption modes.
|
||||
EOF
|
||||
|
||||
# Postinst: refresh icon cache + desktop database, print first-run guidance
|
||||
cat > "$ROOT/DEBIAN/postinst" <<'POSTINST'
|
||||
#!/bin/sh
|
||||
set -e
|
||||
if [ -x /usr/bin/update-desktop-database ]; then
|
||||
update-desktop-database -q /usr/share/applications || true
|
||||
fi
|
||||
if [ -x /usr/bin/gtk-update-icon-cache ]; then
|
||||
gtk-update-icon-cache -q /usr/share/icons/hicolor || true
|
||||
fi
|
||||
|
||||
# Friendly first-run check: warn the user if no Qt6 binding is installed.
|
||||
# We don't fail the install (deb deps already enforce this); we just print
|
||||
# clear guidance for users who saw "unmet dependencies" earlier.
|
||||
if ! python3 -c 'import PyQt6.QtWidgets' 2>/dev/null \
|
||||
&& ! python3 -c 'import PySide6.QtWidgets' 2>/dev/null; then
|
||||
cat << 'MSG'
|
||||
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
zupt-gui installed, but no Qt6 Python binding is available.
|
||||
|
||||
Install one of the following to enable the GUI:
|
||||
|
||||
Debian/Ubuntu/Mint: sudo apt install python3-pyqt6
|
||||
Fedora/RHEL/Rocky: sudo dnf install python3-pyqt6
|
||||
Arch/Manjaro: sudo pacman -S python-pyqt6
|
||||
pip (any distro): pip install --user PySide6
|
||||
|
||||
After installing the binding, launch with: zupt-gui
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
|
||||
MSG
|
||||
fi
|
||||
|
||||
# Same friendly warning if zupt CLI not installed.
|
||||
if ! command -v zupt >/dev/null 2>&1; then
|
||||
cat << 'MSG'
|
||||
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
zupt-gui needs the 'zupt' CLI to function. Install it:
|
||||
|
||||
Debian/Ubuntu/Mint: sudo dpkg -i zupt_2.2.2_amd64.deb
|
||||
(followed by: sudo apt --fix-broken install)
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
|
||||
MSG
|
||||
fi
|
||||
exit 0
|
||||
POSTINST
|
||||
chmod 755 "$ROOT/DEBIAN/postinst"
|
||||
|
||||
cat > "$ROOT/DEBIAN/postrm" <<'POSTRM'
|
||||
#!/bin/sh
|
||||
set -e
|
||||
if [ "$1" = "remove" ] || [ "$1" = "purge" ]; then
|
||||
if [ -x /usr/bin/update-desktop-database ]; then
|
||||
update-desktop-database -q /usr/share/applications || true
|
||||
fi
|
||||
if [ -x /usr/bin/gtk-update-icon-cache ]; then
|
||||
gtk-update-icon-cache -q /usr/share/icons/hicolor || true
|
||||
fi
|
||||
fi
|
||||
POSTRM
|
||||
chmod 755 "$ROOT/DEBIAN/postrm"
|
||||
|
||||
dpkg-deb -Zxz --build --root-owner-group "$ROOT" "/tmp/$PKG.deb"
|
||||
echo "Built: /tmp/$PKG.deb"
|
||||
dpkg-deb --info "/tmp/$PKG.deb" | head -12
|
||||
132
packaging/build-gui-rpm.sh
Executable file
132
packaging/build-gui-rpm.sh
Executable file
|
|
@ -0,0 +1,132 @@
|
|||
#!/bin/bash
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
# Build zupt-gui RPM. Falls back to SRPM-equivalent tarball if rpmbuild absent.
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
VERSION="${VERSION:-1.1.1}"
|
||||
RPMROOT="/tmp/rpmbuild-zupt-gui"
|
||||
rm -rf "$RPMROOT"
|
||||
mkdir -p "$RPMROOT"/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
|
||||
|
||||
TMP="/tmp/zupt-gui-$VERSION"
|
||||
rm -rf "$TMP" && mkdir -p "$TMP/src" "$TMP/doc" "$TMP/assets"
|
||||
cp gui/src/zupt_gui.py "$TMP/src/"
|
||||
cp doc/zupt-gui.1 "$TMP/doc/" 2>/dev/null || true
|
||||
cp gui/README.md "$TMP/" 2>/dev/null || true
|
||||
cp LICENSE "$TMP/" 2>/dev/null || true
|
||||
[ -f gui/assets/zupt-icon.png ] && cp gui/assets/zupt-icon.png "$TMP/assets/"
|
||||
tar -czf "$RPMROOT/SOURCES/zupt-gui-$VERSION.tar.gz" -C /tmp "zupt-gui-$VERSION"
|
||||
|
||||
cat > "$RPMROOT/SPECS/zupt-gui.spec" <<EOF
|
||||
Name: zupt-gui
|
||||
Version: $VERSION
|
||||
Release: 1%{?dist}
|
||||
Summary: Graphical interface for the Zupt post-quantum backup utility
|
||||
License: AGPL-3.0-or-later
|
||||
URL: https://git.securityops.co/cristiancmoises/zupt
|
||||
Source0: zupt-gui-%{version}.tar.gz
|
||||
BuildArch: noarch
|
||||
|
||||
BuildRequires: python3 >= 3.9
|
||||
Requires: python3 >= 3.9
|
||||
Requires: (python3-qt6 or python3-pyside6 or python3-pyqt6)
|
||||
Requires: zupt >= 2.2.2
|
||||
|
||||
%description
|
||||
PySide6/PyQt6 frontend for Zupt. Supports compression, extraction, key
|
||||
management, and full disk backup/restore. Exposes both legacy --pq and
|
||||
new --pq-sdk (libzuptsdk: HKDF combiner, key commitment, HPKE binding,
|
||||
Argon2id) encryption modes. Auto-detects whichever Qt6 binding is
|
||||
installed at startup.
|
||||
|
||||
%prep
|
||||
%autosetup
|
||||
|
||||
%build
|
||||
# nothing to build; pure Python
|
||||
|
||||
%install
|
||||
mkdir -p %{buildroot}%{_bindir}
|
||||
mkdir -p %{buildroot}%{_libdir}/zupt-gui
|
||||
mkdir -p %{buildroot}%{_datadir}/applications
|
||||
mkdir -p %{buildroot}%{_datadir}/icons/hicolor/256x256/apps
|
||||
mkdir -p %{buildroot}%{_mandir}/man1
|
||||
|
||||
install -m 644 src/zupt_gui.py %{buildroot}%{_libdir}/zupt-gui/
|
||||
|
||||
cat > %{buildroot}%{_bindir}/zupt-gui <<'WRAP'
|
||||
#!/bin/sh
|
||||
exec python3 %{_libdir}/zupt-gui/zupt_gui.py "\$@"
|
||||
WRAP
|
||||
chmod 755 %{buildroot}%{_bindir}/zupt-gui
|
||||
|
||||
cat > %{buildroot}%{_datadir}/applications/zupt-gui.desktop <<'DESKTOP'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Zupt GUI
|
||||
GenericName=Backup and Compression Utility
|
||||
Comment=Post-quantum backup with HKDF combiner, key commitment, HPKE binding
|
||||
Exec=zupt-gui %f
|
||||
Icon=zupt-gui
|
||||
Terminal=false
|
||||
Categories=Utility;Archiving;Compression;Security;
|
||||
StartupNotify=true
|
||||
DESKTOP
|
||||
|
||||
[ -f doc/zupt-gui.1 ] && install -m 644 doc/zupt-gui.1 %{buildroot}%{_mandir}/man1/
|
||||
[ -f assets/zupt-icon.png ] && install -m 644 assets/zupt-icon.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/zupt-gui.png || true
|
||||
|
||||
# Generate placeholder icon if no real one exists
|
||||
if [ ! -f %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/zupt-gui.png ]; then
|
||||
python3 -c "
|
||||
import struct, zlib
|
||||
def png(w, h, color):
|
||||
raw = b''.join(b'\\0' + bytes(color) * w for _ in range(h))
|
||||
def chunk(t, d): return struct.pack('>I', len(d)) + t + d + struct.pack('>I', zlib.crc32(t+d) & 0xffffffff)
|
||||
return b'\\x89PNG\\r\\n\\x1a\\n' + chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 0)) + chunk(b'IDAT', zlib.compress(raw)) + chunk(b'IEND', b'')
|
||||
open('%{buildroot}%{_datadir}/icons/hicolor/256x256/apps/zupt-gui.png','wb').write(png(256, 256, (88, 92, 215)))
|
||||
"
|
||||
fi
|
||||
|
||||
%post
|
||||
if [ -x /usr/bin/update-desktop-database ]; then
|
||||
update-desktop-database -q /usr/share/applications || :
|
||||
fi
|
||||
if [ -x /usr/bin/gtk-update-icon-cache ]; then
|
||||
gtk-update-icon-cache -q /usr/share/icons/hicolor || :
|
||||
fi
|
||||
|
||||
%postun
|
||||
if [ \$1 -eq 0 ]; then
|
||||
if [ -x /usr/bin/update-desktop-database ]; then
|
||||
update-desktop-database -q /usr/share/applications || :
|
||||
fi
|
||||
fi
|
||||
|
||||
%files
|
||||
%doc README.md
|
||||
%license LICENSE
|
||||
%{_bindir}/zupt-gui
|
||||
%{_libdir}/zupt-gui/zupt_gui.py
|
||||
%{_datadir}/applications/zupt-gui.desktop
|
||||
%{_datadir}/icons/hicolor/256x256/apps/zupt-gui.png
|
||||
%{_mandir}/man1/zupt-gui.1*
|
||||
|
||||
%changelog
|
||||
* Mon Apr 27 2026 Cristian Cezar Moisés <zupt@riseup.net> - $VERSION-1
|
||||
- Cross-binding (PySide6 OR PyQt6 auto-detected)
|
||||
- SDK v2 mode toggles in compress/extract/keygen tabs
|
||||
- Man page added
|
||||
EOF
|
||||
|
||||
if command -v rpmbuild >/dev/null 2>&1; then
|
||||
rpmbuild --define "_topdir $RPMROOT" -bb "$RPMROOT/SPECS/zupt-gui.spec" 2>&1 | tail -3
|
||||
cp "$RPMROOT/RPMS/noarch/zupt-gui-$VERSION-1."*.rpm /tmp/ 2>/dev/null || true
|
||||
ls /tmp/zupt-gui-$VERSION-*.rpm 2>/dev/null
|
||||
else
|
||||
SRPM_TAR="/tmp/zupt-gui-$VERSION.srpm.tar.gz"
|
||||
tar -czf "$SRPM_TAR" -C "$RPMROOT" SPECS SOURCES
|
||||
echo "rpmbuild unavailable; SRPM-equivalent at: $SRPM_TAR"
|
||||
fi
|
||||
339
packaging/build-rpm-manual.py
Normal file
339
packaging/build-rpm-manual.py
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
"""
|
||||
Build a binary RPM for zupt without rpmbuild.
|
||||
Constructs an RPM-format file directly from the file tree we have for deb.
|
||||
|
||||
This is intentionally minimal but produces a valid RPM that:
|
||||
- Can be installed via `rpm -i` on RHEL/Fedora and other RPM-based distributions
|
||||
- Contains correct dependency info
|
||||
- Has working pre/post scripts
|
||||
- Includes the binary, library, headers, docs, license
|
||||
"""
|
||||
import struct, os, sys, hashlib, gzip, io, time, subprocess
|
||||
|
||||
VERSION = os.environ.get('VERSION', '2.2.1')
|
||||
RELEASE = '1'
|
||||
ARCH = 'x86_64'
|
||||
NAME = 'zupt'
|
||||
|
||||
# RPM tag values (from rpmtag.h)
|
||||
RPMTAG_NAME = 1000
|
||||
RPMTAG_VERSION = 1001
|
||||
RPMTAG_RELEASE = 1002
|
||||
RPMTAG_SUMMARY = 1004
|
||||
RPMTAG_DESCRIPTION = 1005
|
||||
RPMTAG_BUILDTIME = 1006
|
||||
RPMTAG_BUILDHOST = 1007
|
||||
RPMTAG_SIZE = 1009
|
||||
RPMTAG_DISTRIBUTION = 1010
|
||||
RPMTAG_VENDOR = 1011
|
||||
RPMTAG_LICENSE = 1014
|
||||
RPMTAG_PACKAGER = 1015
|
||||
RPMTAG_GROUP = 1016
|
||||
RPMTAG_URL = 1020
|
||||
RPMTAG_OS = 1021
|
||||
RPMTAG_ARCH = 1022
|
||||
RPMTAG_PREIN = 1023
|
||||
RPMTAG_POSTIN = 1024
|
||||
RPMTAG_PREUN = 1025
|
||||
RPMTAG_POSTUN = 1026
|
||||
RPMTAG_FILESIZES = 1028
|
||||
RPMTAG_FILEMODES = 1030
|
||||
RPMTAG_FILERDEVS = 1033
|
||||
RPMTAG_FILEMTIMES = 1034
|
||||
RPMTAG_FILEDIGESTS = 1035
|
||||
RPMTAG_FILELINKTOS = 1036
|
||||
RPMTAG_FILEFLAGS = 1037
|
||||
RPMTAG_FILEUSERNAME = 1039
|
||||
RPMTAG_FILEGROUPNAME = 1040
|
||||
RPMTAG_PROVIDENAME = 1047
|
||||
RPMTAG_REQUIREFLAGS = 1048
|
||||
RPMTAG_REQUIRENAME = 1049
|
||||
RPMTAG_REQUIREVERSION = 1050
|
||||
RPMTAG_BASENAMES = 1117
|
||||
RPMTAG_DIRNAMES = 1118
|
||||
RPMTAG_DIRINDEXES = 1116
|
||||
RPMTAG_PAYLOADFORMAT = 1124
|
||||
RPMTAG_PAYLOADCOMPRESSOR = 1125
|
||||
RPMTAG_FILEDIGESTALGO = 5011
|
||||
|
||||
# Type codes
|
||||
RPM_NULL_TYPE = 0
|
||||
RPM_CHAR_TYPE = 1
|
||||
RPM_INT8_TYPE = 2
|
||||
RPM_INT16_TYPE = 3
|
||||
RPM_INT32_TYPE = 4
|
||||
RPM_INT64_TYPE = 5
|
||||
RPM_STRING_TYPE = 6
|
||||
RPM_BIN_TYPE = 7
|
||||
RPM_STRING_ARRAY_TYPE = 8
|
||||
|
||||
class Header:
|
||||
def __init__(self):
|
||||
self.entries = [] # (tag, type, value)
|
||||
|
||||
def add(self, tag, typ, value):
|
||||
self.entries.append((tag, typ, value))
|
||||
|
||||
def serialize(self):
|
||||
# Build store + index
|
||||
store = bytearray()
|
||||
index = []
|
||||
for tag, typ, value in self.entries:
|
||||
if typ == RPM_STRING_TYPE:
|
||||
count = 1
|
||||
data = value.encode('utf-8') + b'\x00'
|
||||
offset = len(store)
|
||||
store.extend(data)
|
||||
elif typ == RPM_STRING_ARRAY_TYPE:
|
||||
count = len(value)
|
||||
data = b''.join(s.encode('utf-8') + b'\x00' for s in value)
|
||||
offset = len(store)
|
||||
store.extend(data)
|
||||
elif typ == RPM_INT32_TYPE:
|
||||
if not isinstance(value, list):
|
||||
value = [value]
|
||||
count = len(value)
|
||||
# align to 4
|
||||
while len(store) % 4: store.append(0)
|
||||
offset = len(store)
|
||||
for v in value:
|
||||
store.extend(struct.pack('>I', v & 0xFFFFFFFF))
|
||||
elif typ == RPM_INT16_TYPE:
|
||||
if not isinstance(value, list):
|
||||
value = [value]
|
||||
count = len(value)
|
||||
while len(store) % 2: store.append(0)
|
||||
offset = len(store)
|
||||
for v in value:
|
||||
store.extend(struct.pack('>H', v & 0xFFFF))
|
||||
elif typ == RPM_BIN_TYPE:
|
||||
count = len(value)
|
||||
offset = len(store)
|
||||
store.extend(value)
|
||||
elif typ == RPM_NULL_TYPE:
|
||||
count = 1
|
||||
offset = 0
|
||||
else:
|
||||
raise ValueError(f"Unsupported type {typ}")
|
||||
index.append(struct.pack('>IIII', tag, typ, offset, count))
|
||||
|
||||
index_bytes = b''.join(index)
|
||||
# Header magic + reserved + index count + store size
|
||||
out = struct.pack('>3sBI4sII', b'\x8e\xad\xe8', 1, 0, b'\x00\x00\x00\x00',
|
||||
len(self.entries), len(store))
|
||||
out += index_bytes + bytes(store)
|
||||
return out
|
||||
|
||||
def make_cpio(file_list, source_root, payload_size_out):
|
||||
"""Build a cpio archive (newc format) of the files."""
|
||||
out = io.BytesIO()
|
||||
inode = 1
|
||||
total = 0
|
||||
for arc_path, src_path, mode, is_dir, link_target in file_list:
|
||||
if is_dir:
|
||||
data = b''
|
||||
file_size = 0
|
||||
elif link_target is not None:
|
||||
data = link_target.encode('utf-8')
|
||||
file_size = len(data)
|
||||
else:
|
||||
with open(src_path, 'rb') as f:
|
||||
data = f.read()
|
||||
file_size = len(data)
|
||||
total += file_size
|
||||
|
||||
name = ('.' + arc_path).encode('utf-8') + b'\x00'
|
||||
# newc header: 110 bytes
|
||||
header = (
|
||||
b'070701'
|
||||
+ format(inode, '08x').encode('ascii')
|
||||
+ format(mode, '08x').encode('ascii')
|
||||
+ b'00000000' # uid
|
||||
+ b'00000000' # gid
|
||||
+ b'00000001' # nlink
|
||||
+ format(int(time.time()), '08x').encode('ascii')
|
||||
+ format(file_size, '08x').encode('ascii')
|
||||
+ b'00000000' * 4 # devmajor/minor + rdevmajor/minor
|
||||
+ format(len(name), '08x').encode('ascii')
|
||||
+ b'00000000' # check
|
||||
)
|
||||
out.write(header)
|
||||
out.write(name)
|
||||
# pad to 4
|
||||
pad = (4 - ((len(header) + len(name)) % 4)) % 4
|
||||
out.write(b'\x00' * pad)
|
||||
out.write(data)
|
||||
# pad data to 4
|
||||
pad = (4 - (file_size % 4)) % 4
|
||||
out.write(b'\x00' * pad)
|
||||
inode += 1
|
||||
|
||||
# Trailer
|
||||
trailer_name = b'TRAILER!!!\x00'
|
||||
out.write(b'070701' + b'0' * 8 + b'0' * 8 + b'0' * 8 + b'0' * 8 + b'00000001'
|
||||
+ b'0' * 8 + b'0' * 8 + b'0' * 8 + b'0' * 8 + b'0' * 8 + b'0' * 8
|
||||
+ format(len(trailer_name), '08x').encode('ascii') + b'0' * 8)
|
||||
out.write(trailer_name)
|
||||
pad = (4 - ((110 + len(trailer_name)) % 4)) % 4
|
||||
out.write(b'\x00' * pad)
|
||||
payload_size_out[0] = total
|
||||
return out.getvalue()
|
||||
|
||||
def main():
|
||||
# Files to include (source_path inside our deb tree)
|
||||
deb_root = '/tmp/zupt_2.2.1_amd64'
|
||||
files = [] # (arc_path, source_path, mode, is_dir, link_target)
|
||||
|
||||
for root, dirs, fnames in os.walk(deb_root):
|
||||
for d in sorted(dirs):
|
||||
full = os.path.join(root, d)
|
||||
arc = full[len(deb_root):]
|
||||
files.append((arc, full, 0o40755, True, None))
|
||||
for fn in sorted(fnames):
|
||||
full = os.path.join(root, fn)
|
||||
arc = full[len(deb_root):]
|
||||
if 'DEBIAN' in arc:
|
||||
continue
|
||||
if os.path.islink(full):
|
||||
files.append((arc, full, 0o120777, False, os.readlink(full)))
|
||||
else:
|
||||
mode = 0o100755 if os.access(full, os.X_OK) else 0o100644
|
||||
files.append((arc, full, mode, False, None))
|
||||
|
||||
# Sort and build basename/dirname/dirindex arrays
|
||||
files.sort(key=lambda x: x[0])
|
||||
|
||||
basenames = []
|
||||
dirnames_set = []
|
||||
dirname_to_idx = {}
|
||||
dirindexes = []
|
||||
filesizes = []
|
||||
filemodes = []
|
||||
filemtimes = []
|
||||
filedigests = []
|
||||
filelinktos = []
|
||||
filerdevs = []
|
||||
fileflags = []
|
||||
fileuser = []
|
||||
filegroup = []
|
||||
|
||||
for arc, src, mode, is_dir, link in files:
|
||||
d, b = os.path.split(arc)
|
||||
d = d + '/'
|
||||
if d not in dirname_to_idx:
|
||||
dirname_to_idx[d] = len(dirnames_set)
|
||||
dirnames_set.append(d)
|
||||
basenames.append(b or '.')
|
||||
dirindexes.append(dirname_to_idx[d])
|
||||
if is_dir:
|
||||
filesizes.append(0)
|
||||
filedigests.append('')
|
||||
filelinktos.append('')
|
||||
elif link:
|
||||
filesizes.append(len(link))
|
||||
filedigests.append('')
|
||||
filelinktos.append(link)
|
||||
else:
|
||||
filesizes.append(os.path.getsize(src))
|
||||
with open(src, 'rb') as f:
|
||||
filedigests.append(hashlib.sha256(f.read()).hexdigest())
|
||||
filelinktos.append('')
|
||||
filemodes.append(mode)
|
||||
filemtimes.append(int(time.time()))
|
||||
filerdevs.append(0)
|
||||
fileflags.append(0)
|
||||
fileuser.append('root')
|
||||
filegroup.append('root')
|
||||
|
||||
payload_size = [0]
|
||||
cpio_data = make_cpio(files, deb_root, payload_size)
|
||||
# Compress payload with gzip
|
||||
gz_payload = gzip.compress(cpio_data)
|
||||
|
||||
# Build main header
|
||||
h = Header()
|
||||
h.add(RPMTAG_NAME, RPM_STRING_TYPE, NAME)
|
||||
h.add(RPMTAG_VERSION, RPM_STRING_TYPE, VERSION)
|
||||
h.add(RPMTAG_RELEASE, RPM_STRING_TYPE, RELEASE)
|
||||
h.add(RPMTAG_SUMMARY, RPM_STRING_ARRAY_TYPE, ['Post-quantum backup compression utility'])
|
||||
h.add(RPMTAG_DESCRIPTION, RPM_STRING_ARRAY_TYPE, [
|
||||
'Zupt provides hybrid post-quantum encryption (ML-KEM-768 + X25519)\n'
|
||||
'with multi-threaded compression and full-disk backup support.\n'
|
||||
'Bundled with libzuptsdk for HKDF-SHA3 hybrid KDF, key commitment,\n'
|
||||
'HPKE binding, and anti-fault decapsulation.'
|
||||
])
|
||||
h.add(RPMTAG_BUILDTIME, RPM_INT32_TYPE, int(time.time()))
|
||||
h.add(RPMTAG_BUILDHOST, RPM_STRING_TYPE, 'localhost')
|
||||
h.add(RPMTAG_SIZE, RPM_INT32_TYPE, sum(filesizes))
|
||||
h.add(RPMTAG_LICENSE, RPM_STRING_TYPE, 'AGPL-3.0-or-later')
|
||||
h.add(RPMTAG_PACKAGER, RPM_STRING_TYPE, 'Cristian Cezar Moises <zupt@riseup.net>')
|
||||
h.add(RPMTAG_GROUP, RPM_STRING_ARRAY_TYPE, ['Applications/Archiving'])
|
||||
h.add(RPMTAG_URL, RPM_STRING_TYPE, 'https://git.securityops.co/cristiancmoises/zupt')
|
||||
h.add(RPMTAG_OS, RPM_STRING_TYPE, 'linux')
|
||||
h.add(RPMTAG_ARCH, RPM_STRING_TYPE, ARCH)
|
||||
h.add(RPMTAG_POSTIN, RPM_STRING_TYPE, '/sbin/ldconfig\n')
|
||||
h.add(RPMTAG_POSTUN, RPM_STRING_TYPE, '/sbin/ldconfig\n')
|
||||
h.add(RPMTAG_BASENAMES, RPM_STRING_ARRAY_TYPE, basenames)
|
||||
h.add(RPMTAG_DIRNAMES, RPM_STRING_ARRAY_TYPE, dirnames_set)
|
||||
h.add(RPMTAG_DIRINDEXES, RPM_INT32_TYPE, dirindexes)
|
||||
h.add(RPMTAG_FILESIZES, RPM_INT32_TYPE, filesizes)
|
||||
h.add(RPMTAG_FILEMODES, RPM_INT16_TYPE, filemodes)
|
||||
h.add(RPMTAG_FILEMTIMES, RPM_INT32_TYPE, filemtimes)
|
||||
h.add(RPMTAG_FILEDIGESTS, RPM_STRING_ARRAY_TYPE, filedigests)
|
||||
h.add(RPMTAG_FILELINKTOS, RPM_STRING_ARRAY_TYPE, filelinktos)
|
||||
h.add(RPMTAG_FILEFLAGS, RPM_INT32_TYPE, fileflags)
|
||||
h.add(RPMTAG_FILERDEVS, RPM_INT16_TYPE, filerdevs)
|
||||
h.add(RPMTAG_FILEUSERNAME, RPM_STRING_ARRAY_TYPE, fileuser)
|
||||
h.add(RPMTAG_FILEGROUPNAME, RPM_STRING_ARRAY_TYPE, filegroup)
|
||||
h.add(RPMTAG_PROVIDENAME, RPM_STRING_ARRAY_TYPE, [NAME])
|
||||
h.add(RPMTAG_REQUIRENAME, RPM_STRING_ARRAY_TYPE, ['libargon2.so.1()(64bit)', 'libcrypto.so.3()(64bit)', 'libc.so.6()(64bit)'])
|
||||
h.add(RPMTAG_REQUIREFLAGS, RPM_INT32_TYPE, [0, 0, 0])
|
||||
h.add(RPMTAG_REQUIREVERSION, RPM_STRING_ARRAY_TYPE, ['', '', ''])
|
||||
h.add(RPMTAG_PAYLOADFORMAT, RPM_STRING_TYPE, 'cpio')
|
||||
h.add(RPMTAG_PAYLOADCOMPRESSOR, RPM_STRING_TYPE, 'gzip')
|
||||
h.add(RPMTAG_FILEDIGESTALGO, RPM_INT32_TYPE, 8) # SHA-256
|
||||
|
||||
main_hdr = h.serialize()
|
||||
|
||||
# Signature header (minimal: just size of payload after sig hdr)
|
||||
sig = Header()
|
||||
sig_payload = main_hdr + gz_payload
|
||||
sig.add(1000, RPM_INT32_TYPE, len(sig_payload)) # SIZE
|
||||
sig.add(1004, RPM_BIN_TYPE, hashlib.md5(sig_payload).digest()) # MD5
|
||||
sig_bytes = sig.serialize()
|
||||
# Pad sig hdr to 8-byte boundary
|
||||
pad = (8 - (len(sig_bytes) % 8)) % 8
|
||||
sig_bytes += b'\x00' * pad
|
||||
|
||||
# Lead (96 bytes)
|
||||
lead = struct.pack('>4sBBhh66sHH16s',
|
||||
b'\xed\xab\xee\xdb', # magic
|
||||
3, 0, # major, minor
|
||||
0, # type (binary)
|
||||
1, # archnum
|
||||
NAME.encode().ljust(66, b'\x00'),
|
||||
1, # osnum
|
||||
5, # signature_type
|
||||
b'\x00' * 16)
|
||||
|
||||
out_path = f'/tmp/{NAME}-{VERSION}-{RELEASE}.{ARCH}.rpm'
|
||||
with open(out_path, 'wb') as f:
|
||||
f.write(lead)
|
||||
f.write(sig_bytes)
|
||||
f.write(main_hdr)
|
||||
f.write(gz_payload)
|
||||
|
||||
print(f'Built: {out_path} ({os.path.getsize(out_path)} bytes)')
|
||||
# Try rpm -Kvv to verify if rpm is installed
|
||||
try:
|
||||
result = subprocess.run(['rpm', '-qpi', out_path], capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
print(result.stdout[:500])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
68
packaging/build-rpm.sh
Executable file
68
packaging/build-rpm.sh
Executable file
|
|
@ -0,0 +1,68 @@
|
|||
#!/bin/bash
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
# Build zupt RPM. Falls back to SRPM-equivalent tarball if rpmbuild absent.
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
VERSION="${VERSION:-2.2.2}"
|
||||
RPMROOT="/tmp/rpmbuild-zupt"
|
||||
rm -rf "$RPMROOT"
|
||||
mkdir -p "$RPMROOT"/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
|
||||
|
||||
TMP="/tmp/zupt-$VERSION"
|
||||
rm -rf "$TMP" && mkdir -p "$TMP"
|
||||
cp -r include src tests jasmin doc vendor Makefile CMakeLists.txt \
|
||||
README.md CHANGELOG.md SECURITY.md AUDIT.md LICENSE "$TMP/" 2>/dev/null || true
|
||||
tar -czf "$RPMROOT/SOURCES/zupt-$VERSION.tar.gz" -C /tmp "zupt-$VERSION"
|
||||
|
||||
cat > "$RPMROOT/SPECS/zupt.spec" <<EOF
|
||||
Name: zupt
|
||||
Version: $VERSION
|
||||
Release: 1%{?dist}
|
||||
Summary: Post-quantum backup compression utility
|
||||
License: AGPL-3.0-or-later AND GPL-3.0-or-later
|
||||
URL: https://git.securityops.co/cristiancmoises/zupt
|
||||
Source0: zupt-%{version}.tar.gz
|
||||
|
||||
BuildRequires: gcc make libargon2-devel openssl-devel >= 3.0 libzuptsdk-devel >= 2.0.0
|
||||
Requires: libargon2 openssl-libs >= 3.0 libzuptsdk2 >= 2.0.0
|
||||
|
||||
%description
|
||||
Backup-oriented compression utility with hybrid post-quantum encryption
|
||||
(ML-KEM-768 + X25519). Integrates libzuptsdk for state-of-the-art crypto:
|
||||
HKDF combiner with domain separation, key commitment, HPKE binding,
|
||||
anti-fault decapsulation, Argon2id password derivation.
|
||||
|
||||
%prep
|
||||
%autosetup
|
||||
|
||||
%build
|
||||
make %{?_smp_mflags}
|
||||
|
||||
%install
|
||||
mkdir -p %{buildroot}%{_bindir}
|
||||
install -m 755 zupt %{buildroot}%{_bindir}/zupt
|
||||
|
||||
%files
|
||||
%license LICENSE
|
||||
%doc README.md CHANGELOG.md SECURITY.md AUDIT.md
|
||||
%{_bindir}/zupt
|
||||
|
||||
%changelog
|
||||
* Mon Apr 27 2026 Cristian Cezar Moisés <zupt@riseup.net> - $VERSION-1
|
||||
- Audit-driven release: 6 bugs fixed (varint truncation, unchecked fwrites,
|
||||
mac_key reuse, LZ length overflow, dedup ref recursion, partial archive
|
||||
cleanup on encrypt failure). 30/30 tests pass.
|
||||
EOF
|
||||
|
||||
if command -v rpmbuild >/dev/null 2>&1; then
|
||||
rpmbuild --define "_topdir $RPMROOT" -bb "$RPMROOT/SPECS/zupt.spec" 2>&1 | tail -5
|
||||
cp "$RPMROOT/RPMS/x86_64/zupt-$VERSION-1."*.rpm /tmp/ 2>/dev/null || true
|
||||
ls /tmp/zupt-$VERSION-*.rpm 2>/dev/null
|
||||
else
|
||||
SRPM_TAR="/tmp/zupt-$VERSION.srpm.tar.gz"
|
||||
tar -czf "$SRPM_TAR" -C "$RPMROOT" SPECS SOURCES
|
||||
echo "rpmbuild unavailable; SRPM-equivalent at: $SRPM_TAR"
|
||||
echo "Distros: tar -xzf $SRPM_TAR && rpmbuild -bb SPECS/zupt.spec"
|
||||
fi
|
||||
139
packaging/install-zupt-gui.sh
Executable file
139
packaging/install-zupt-gui.sh
Executable file
|
|
@ -0,0 +1,139 @@
|
|||
#!/bin/bash
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
# Zupt + Zupt GUI all-in-one installer for Linux
|
||||
# Detects your distro, installs all dependencies, then installs
|
||||
# zupt and zupt-gui. Run as root or with sudo.
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ZUPT_CLI_DEB="$SCRIPT_DIR/zupt_2.2.2_amd64.deb"
|
||||
ZUPT_GUI_DEB="$SCRIPT_DIR/zupt-gui_1.1.1_all.deb"
|
||||
|
||||
print_step() { echo ""; echo "═══ $* ═══"; }
|
||||
print_err() { echo "ERROR: $*" >&2; exit 1; }
|
||||
|
||||
# Must be root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
print_err "Run with sudo: sudo bash $0"
|
||||
fi
|
||||
|
||||
# Detect distro
|
||||
if [ -f /etc/os-release ]; then
|
||||
. /etc/os-release
|
||||
DISTRO="$ID"
|
||||
DISTRO_LIKE="${ID_LIKE:-}"
|
||||
else
|
||||
print_err "Cannot detect distribution (no /etc/os-release)"
|
||||
fi
|
||||
|
||||
print_step "Detected: $PRETTY_NAME"
|
||||
|
||||
# 1. Install Python 3 + Qt6 binding
|
||||
print_step "Step 1/3: Installing Python 3 and Qt6 binding"
|
||||
case "$DISTRO" in
|
||||
debian|ubuntu|linuxmint|pop)
|
||||
apt-get update
|
||||
apt-get install -y python3 python3-pyqt6 || \
|
||||
apt-get install -y python3 python3-pyside6
|
||||
;;
|
||||
fedora|rhel|centos|rocky|almalinux)
|
||||
if command -v dnf >/dev/null; then
|
||||
dnf install -y python3 python3-pyqt6 || dnf install -y python3 python3-pyside6
|
||||
else
|
||||
yum install -y python3 python3-pyqt6 || yum install -y python3 python3-pyside6
|
||||
fi
|
||||
;;
|
||||
opensuse*|suse)
|
||||
zypper install -y python3 python3-pyqt6 || zypper install -y python3 python3-PyQt6 \
|
||||
|| zypper install -y python3 python3-pyside6
|
||||
;;
|
||||
arch|manjaro|endeavouros)
|
||||
pacman -S --noconfirm python python-pyqt6 || pacman -S --noconfirm python python-pyside6
|
||||
;;
|
||||
alpine)
|
||||
apk add python3 py3-pyqt6 || apk add python3 py3-pyside6
|
||||
;;
|
||||
*)
|
||||
# Fallback: try pip
|
||||
echo "Unknown distribution '$DISTRO'. Trying pip fallback..."
|
||||
if command -v pip3 >/dev/null; then
|
||||
pip3 install --break-system-packages PySide6 || pip3 install PySide6
|
||||
else
|
||||
print_err "No pip3 available. Install python3-pyqt6 manually for your distro."
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# Verify Qt6 binding works
|
||||
if ! python3 -c 'import PyQt6.QtWidgets' 2>/dev/null \
|
||||
&& ! python3 -c 'import PySide6.QtWidgets' 2>/dev/null; then
|
||||
print_err "Failed to install Qt6 Python binding. Install manually with your package manager."
|
||||
fi
|
||||
echo "✓ Python 3 + Qt6 binding installed"
|
||||
|
||||
# 2. Install zupt CLI
|
||||
print_step "Step 2/3: Installing zupt CLI 2.2.2"
|
||||
case "$DISTRO" in
|
||||
debian|ubuntu|linuxmint|pop)
|
||||
if [ ! -f "$ZUPT_CLI_DEB" ]; then
|
||||
print_err "Cannot find $ZUPT_CLI_DEB next to this script"
|
||||
fi
|
||||
# Force-replace any older zupt
|
||||
dpkg -i "$ZUPT_CLI_DEB" || apt-get -f install -y
|
||||
;;
|
||||
fedora|rhel|centos|rocky|almalinux|opensuse*|suse)
|
||||
ZUPT_CLI_RPM="$SCRIPT_DIR/zupt-2.2.2-1.x86_64.rpm"
|
||||
if [ -f "$ZUPT_CLI_RPM" ]; then
|
||||
rpm -Uvh --force "$ZUPT_CLI_RPM"
|
||||
else
|
||||
print_err "RPM build not provided. Build from source tarball or install via SRPM."
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# Fallback: tarball install
|
||||
ZUPT_CLI_TAR="$SCRIPT_DIR/zupt-2.2.2-linux-x86_64.tar.gz"
|
||||
if [ -f "$ZUPT_CLI_TAR" ]; then
|
||||
tar -xzf "$ZUPT_CLI_TAR" -C /opt/
|
||||
ln -sf /opt/zupt-2.2.2-linux-x86_64/zupt /usr/local/bin/zupt
|
||||
else
|
||||
print_err "No suitable installer for $DISTRO"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
echo "✓ zupt CLI installed"
|
||||
|
||||
# 3. Install zupt-gui
|
||||
print_step "Step 3/3: Installing zupt-gui"
|
||||
case "$DISTRO" in
|
||||
debian|ubuntu|linuxmint|pop)
|
||||
dpkg -i "$ZUPT_GUI_DEB" || apt-get -f install -y
|
||||
;;
|
||||
fedora|rhel|centos|rocky|almalinux|opensuse*|suse)
|
||||
ZUPT_GUI_RPM="$SCRIPT_DIR/zupt-gui-1.1.1-1.noarch.rpm"
|
||||
if [ -f "$ZUPT_GUI_RPM" ]; then
|
||||
rpm -Uvh --force "$ZUPT_GUI_RPM"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# Manual fallback
|
||||
mkdir -p /opt/zupt-gui /usr/local/bin
|
||||
cp "$SCRIPT_DIR/zupt_gui.py" /opt/zupt-gui/ 2>/dev/null || true
|
||||
cat > /usr/local/bin/zupt-gui <<'WRAP'
|
||||
#!/bin/sh
|
||||
exec python3 /opt/zupt-gui/zupt_gui.py "$@"
|
||||
WRAP
|
||||
chmod +x /usr/local/bin/zupt-gui
|
||||
;;
|
||||
esac
|
||||
echo "✓ zupt-gui installed"
|
||||
|
||||
print_step "Installation complete"
|
||||
echo ""
|
||||
echo "Run:"
|
||||
echo " zupt help # CLI help"
|
||||
echo " zupt-gui # Graphical interface"
|
||||
echo ""
|
||||
echo "If you encounter issues, check that your zupt version is correct:"
|
||||
echo " zupt version # should show 2.2.2"
|
||||
312
packaging/zupt-installer-header.sh
Normal file
312
packaging/zupt-installer-header.sh
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
#!/bin/bash
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
# ╔════════════════════════════════════════════════════════════════════╗
|
||||
# ║ ZUPT 2.2.2 + ZUPT-GUI 1.1.1 — UNIVERSAL LINUX INSTALLER ║
|
||||
# ║ ║
|
||||
# ║ One script, all distributions. Self-extracting. No internet ║
|
||||
# ║ needed for the package install (only for Qt6 dependency). ║
|
||||
# ║ ║
|
||||
# ║ Usage: sudo bash zupt-installer.sh ║
|
||||
# ║ Or: sudo bash zupt-installer.sh --gui-only ║
|
||||
# ║ Or: sudo bash zupt-installer.sh --cli-only ║
|
||||
# ║ Or: sudo bash zupt-installer.sh --appimage ║
|
||||
# ║ Or: sudo bash zupt-installer.sh --uninstall ║
|
||||
# ╚════════════════════════════════════════════════════════════════════╝
|
||||
set -e
|
||||
|
||||
VERSION="2.2.2"
|
||||
GUI_VERSION="1.1.1"
|
||||
EXTRACT_DIR=""
|
||||
|
||||
cleanup() {
|
||||
[ -n "$EXTRACT_DIR" ] && [ -d "$EXTRACT_DIR" ] && rm -rf "$EXTRACT_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# ── Color output (if terminal supports) ─────────────────────────────
|
||||
if [ -t 1 ]; then
|
||||
BOLD='\033[1m'; CYAN='\033[36m'; GREEN='\033[32m'; YELLOW='\033[33m'; RED='\033[31m'; RESET='\033[0m'
|
||||
else
|
||||
BOLD=''; CYAN=''; GREEN=''; YELLOW=''; RED=''; RESET=''
|
||||
fi
|
||||
|
||||
step() { echo -e "${CYAN}${BOLD}═══ $* ═══${RESET}"; }
|
||||
ok() { echo -e "${GREEN}✓${RESET} $*"; }
|
||||
warn() { echo -e "${YELLOW}⚠${RESET} $*"; }
|
||||
err() { echo -e "${RED}✗${RESET} $*" >&2; }
|
||||
die() { err "$*"; exit 1; }
|
||||
|
||||
# ── Parse arguments ─────────────────────────────────────────────────
|
||||
MODE="full"
|
||||
case "${1:-}" in
|
||||
--cli-only) MODE="cli" ;;
|
||||
--gui-only) MODE="gui" ;;
|
||||
--appimage) MODE="appimage" ;;
|
||||
--uninstall) MODE="uninstall" ;;
|
||||
--help|-h)
|
||||
sed -n '2,15p' "$0" | sed 's/^# //'
|
||||
exit 0 ;;
|
||||
"") MODE="full" ;;
|
||||
*) die "Unknown option: $1. Use --help for options." ;;
|
||||
esac
|
||||
|
||||
# ── Root check (except for AppImage) ────────────────────────────────
|
||||
if [ "$MODE" != "appimage" ] && [ "$EUID" -ne 0 ]; then
|
||||
die "Run with sudo: sudo bash $0 ${1:-}"
|
||||
fi
|
||||
|
||||
# ── Distro detection ────────────────────────────────────────────────
|
||||
detect_distro() {
|
||||
if [ -f /etc/os-release ]; then
|
||||
# Use subshell to prevent /etc/os-release VERSION from clobbering ours
|
||||
DISTRO=$(. /etc/os-release; echo "${ID:-unknown}")
|
||||
DISTRO_LIKE=$(. /etc/os-release; echo "${ID_LIKE:-}")
|
||||
DISTRO_NAME=$(. /etc/os-release; echo "${PRETTY_NAME:-$DISTRO}")
|
||||
else
|
||||
DISTRO="unknown"; DISTRO_LIKE=""; DISTRO_NAME="Unknown Linux"
|
||||
fi
|
||||
}
|
||||
detect_distro
|
||||
|
||||
# Categorize
|
||||
DEB_BASED=0; RPM_BASED=0; ARCH_BASED=0; ALPINE=0
|
||||
case "$DISTRO" in
|
||||
debian|ubuntu|linuxmint|pop|elementary|kali|raspbian|deepin|zorin) DEB_BASED=1 ;;
|
||||
fedora|rhel|centos|rocky|almalinux|ol) RPM_BASED=1 ;;
|
||||
opensuse*|suse|sles) RPM_BASED=1 ;;
|
||||
arch|manjaro|endeavouros|garuda|artix) ARCH_BASED=1 ;;
|
||||
alpine) ALPINE=1 ;;
|
||||
*)
|
||||
case "$DISTRO_LIKE" in
|
||||
*debian*|*ubuntu*) DEB_BASED=1 ;;
|
||||
*fedora*|*rhel*|*suse*) RPM_BASED=1 ;;
|
||||
*arch*) ARCH_BASED=1 ;;
|
||||
esac ;;
|
||||
esac
|
||||
|
||||
# ── Self-extract embedded payload ───────────────────────────────────
|
||||
extract_payload() {
|
||||
EXTRACT_DIR=$(mktemp -d -t zupt-installer.XXXXXX)
|
||||
# Find the line number where the payload starts (marker: __PAYLOAD_BELOW__)
|
||||
local marker_line
|
||||
marker_line=$(grep -an '^__PAYLOAD_BELOW__$' "$0" | head -1 | cut -d: -f1)
|
||||
[ -z "$marker_line" ] && die "Installer is corrupt — no payload marker."
|
||||
# Skip past marker line, decode base64 → tar
|
||||
tail -n +$((marker_line + 1)) "$0" | base64 -d | tar -xzC "$EXTRACT_DIR"
|
||||
[ -f "$EXTRACT_DIR/zupt_${VERSION}_amd64.deb" ] || die "Payload extraction failed."
|
||||
}
|
||||
|
||||
# ── Install Qt6 binding (needs network) ─────────────────────────────
|
||||
install_qt6() {
|
||||
if python3 -c 'import PyQt6.QtWidgets' 2>/dev/null \
|
||||
|| python3 -c 'import PySide6.QtWidgets' 2>/dev/null; then
|
||||
ok "Qt6 binding already installed"
|
||||
return 0
|
||||
fi
|
||||
step "Installing Python 3 + Qt6 binding"
|
||||
if [ $DEB_BASED -eq 1 ]; then
|
||||
apt-get update -qq || warn "apt-get update failed (network?); continuing anyway"
|
||||
apt-get install -y python3 python3-pyqt6 \
|
||||
|| apt-get install -y python3 python3-pyside6 \
|
||||
|| warn "Could not install Qt6 binding via apt"
|
||||
elif [ $RPM_BASED -eq 1 ]; then
|
||||
case "$DISTRO" in
|
||||
opensuse*|suse|sles)
|
||||
zypper --non-interactive install python3 python3-pyqt6 \
|
||||
|| zypper --non-interactive install python3 python3-PyQt6 \
|
||||
|| zypper --non-interactive install python3 python3-pyside6 ;;
|
||||
*)
|
||||
if command -v dnf >/dev/null; then
|
||||
dnf install -y python3 python3-pyqt6 \
|
||||
|| dnf install -y python3 python3-pyside6
|
||||
else
|
||||
yum install -y python3 python3-pyqt6 \
|
||||
|| yum install -y python3 python3-pyside6
|
||||
fi ;;
|
||||
esac
|
||||
elif [ $ARCH_BASED -eq 1 ]; then
|
||||
pacman -Sy --noconfirm python python-pyqt6 \
|
||||
|| pacman -Sy --noconfirm python python-pyside6
|
||||
elif [ $ALPINE -eq 1 ]; then
|
||||
apk add python3 py3-pyqt6 || apk add python3 py3-pyside6
|
||||
else
|
||||
warn "Unknown distribution. Trying pip fallback..."
|
||||
if command -v pip3 >/dev/null; then
|
||||
pip3 install --break-system-packages PySide6 2>/dev/null \
|
||||
|| pip3 install --user PySide6
|
||||
else
|
||||
warn "No pip3. Install python3-pyqt6 manually."
|
||||
fi
|
||||
fi
|
||||
if python3 -c 'import PyQt6.QtWidgets' 2>/dev/null \
|
||||
|| python3 -c 'import PySide6.QtWidgets' 2>/dev/null; then
|
||||
ok "Qt6 binding installed"
|
||||
else
|
||||
warn "Qt6 binding install failed. The CLI will still work; the GUI won't."
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Install zupt CLI ────────────────────────────────────────────────
|
||||
install_cli() {
|
||||
step "Installing zupt CLI ${VERSION}"
|
||||
if [ $DEB_BASED -eq 1 ]; then
|
||||
dpkg -i "$EXTRACT_DIR/zupt_${VERSION}_amd64.deb" 2>&1 \
|
||||
| grep -v '^Selecting\|^Preparing\|^Unpacking\|^Setting up\|^Processing' || true
|
||||
# Resolve any missing libs from apt
|
||||
apt-get -f install -y 2>/dev/null || true
|
||||
ok "zupt CLI installed: $(zupt version 2>&1 | head -1)"
|
||||
elif [ $RPM_BASED -eq 1 ]; then
|
||||
local rpmtar="$EXTRACT_DIR/zupt-${VERSION}.srpm.tar.gz"
|
||||
if command -v rpmbuild >/dev/null; then
|
||||
local rpmroot=$(mktemp -d)
|
||||
tar -xzC "$rpmroot" -f "$rpmtar"
|
||||
rpmbuild --define "_topdir $rpmroot" -bb "$rpmroot/SPECS/zupt.spec"
|
||||
rpm -Uvh --force "$rpmroot"/RPMS/x86_64/zupt-*.rpm
|
||||
rm -rf "$rpmroot"
|
||||
else
|
||||
# rpmbuild not available — fall back to tarball
|
||||
warn "rpmbuild missing — using portable binary install"
|
||||
local appdir="$EXTRACT_DIR/zupt-${VERSION}-x86_64.AppDir.tar.gz"
|
||||
mkdir -p /opt
|
||||
tar -xzC /opt -f "$appdir"
|
||||
ln -sf "/opt/zupt-${VERSION}-x86_64.AppDir/AppRun" /usr/local/bin/zupt
|
||||
ok "zupt CLI installed (portable mode)"
|
||||
fi
|
||||
else
|
||||
# Universal fallback: portable AppDir tarball
|
||||
warn "No native package format for $DISTRO. Using portable binary."
|
||||
mkdir -p /opt /usr/local/bin
|
||||
tar -xzC /opt -f "$EXTRACT_DIR/zupt-${VERSION}-x86_64.AppDir.tar.gz"
|
||||
ln -sf "/opt/zupt-${VERSION}-x86_64.AppDir/AppRun" /usr/local/bin/zupt
|
||||
ok "zupt CLI installed (portable mode)"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Install zupt-gui ────────────────────────────────────────────────
|
||||
install_gui() {
|
||||
step "Installing zupt-gui ${GUI_VERSION}"
|
||||
if [ $DEB_BASED -eq 1 ]; then
|
||||
dpkg -i "$EXTRACT_DIR/zupt-gui_${GUI_VERSION}_all.deb" 2>&1 \
|
||||
| grep -v '^Selecting\|^Preparing\|^Unpacking\|^Setting up\|^Processing' || true
|
||||
apt-get -f install -y 2>/dev/null || true
|
||||
ok "zupt-gui installed"
|
||||
elif [ $RPM_BASED -eq 1 ]; then
|
||||
local rpmtar="$EXTRACT_DIR/zupt-gui-${GUI_VERSION}.srpm.tar.gz"
|
||||
if command -v rpmbuild >/dev/null; then
|
||||
local rpmroot=$(mktemp -d)
|
||||
tar -xzC "$rpmroot" -f "$rpmtar"
|
||||
rpmbuild --define "_topdir $rpmroot" -bb "$rpmroot/SPECS/zupt-gui.spec"
|
||||
rpm -Uvh --force "$rpmroot"/RPMS/noarch/zupt-gui-*.rpm
|
||||
rm -rf "$rpmroot"
|
||||
else
|
||||
warn "rpmbuild missing — using portable mode"
|
||||
mkdir -p /opt /usr/local/bin
|
||||
tar -xzC /opt -f "$EXTRACT_DIR/Zupt-GUI-${GUI_VERSION}-x86_64.AppDir.tar.gz"
|
||||
ln -sf "/opt/zupt-gui.AppDir/AppRun" /usr/local/bin/zupt-gui
|
||||
ok "zupt-gui installed (portable)"
|
||||
fi
|
||||
else
|
||||
# Portable
|
||||
mkdir -p /opt /usr/local/bin
|
||||
tar -xzC /opt -f "$EXTRACT_DIR/Zupt-GUI-${GUI_VERSION}-x86_64.AppDir.tar.gz"
|
||||
ln -sf "/opt/zupt-gui.AppDir/AppRun" /usr/local/bin/zupt-gui
|
||||
# Desktop integration if possible
|
||||
if [ -d /usr/share/applications ]; then
|
||||
cp /opt/zupt-gui.AppDir/zupt-gui.desktop /usr/share/applications/ 2>/dev/null || true
|
||||
fi
|
||||
ok "zupt-gui installed (portable)"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── AppImage extract (no install) ───────────────────────────────────
|
||||
install_appimage() {
|
||||
step "Extracting AppImage to current directory"
|
||||
local target="${PWD}/zupt-portable"
|
||||
mkdir -p "$target"
|
||||
tar -xzC "$target" -f "$EXTRACT_DIR/zupt-${VERSION}-x86_64.AppDir.tar.gz"
|
||||
tar -xzC "$target" -f "$EXTRACT_DIR/Zupt-GUI-${GUI_VERSION}-x86_64.AppDir.tar.gz"
|
||||
cat > "$target/zupt" <<EOF
|
||||
#!/bin/sh
|
||||
exec "$target/zupt-${VERSION}-x86_64.AppDir/AppRun" "\$@"
|
||||
EOF
|
||||
cat > "$target/zupt-gui" <<EOF
|
||||
#!/bin/sh
|
||||
exec "$target/zupt-gui.AppDir/AppRun" "\$@"
|
||||
EOF
|
||||
chmod +x "$target/zupt" "$target/zupt-gui"
|
||||
ok "Portable install at: $target"
|
||||
echo "Run: $target/zupt help"
|
||||
echo " $target/zupt-gui"
|
||||
echo
|
||||
warn "Portable mode still needs Python 3 + PyQt6 system-wide."
|
||||
warn "To install Qt6: sudo apt install python3-pyqt6 (or equivalent)"
|
||||
}
|
||||
|
||||
# ── Uninstall ───────────────────────────────────────────────────────
|
||||
do_uninstall() {
|
||||
step "Uninstalling zupt + zupt-gui"
|
||||
if [ $DEB_BASED -eq 1 ]; then
|
||||
dpkg -r zupt-gui 2>/dev/null || true
|
||||
dpkg -r zupt 2>/dev/null || true
|
||||
elif [ $RPM_BASED -eq 1 ]; then
|
||||
rpm -e zupt-gui 2>/dev/null || true
|
||||
rpm -e zupt 2>/dev/null || true
|
||||
fi
|
||||
rm -rf /opt/zupt-2.2.2-x86_64.AppDir /opt/zupt-gui.AppDir 2>/dev/null
|
||||
rm -f /usr/local/bin/zupt /usr/local/bin/zupt-gui 2>/dev/null
|
||||
rm -f /usr/share/applications/zupt-gui.desktop 2>/dev/null
|
||||
ok "Uninstall complete"
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# MAIN
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
cat <<HEADER
|
||||
${BOLD}${CYAN}
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ ZUPT ${VERSION} + ZUPT-GUI ${GUI_VERSION} ║
|
||||
║ Post-quantum backup compression — Linux installer ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
${RESET}
|
||||
Distribution detected: ${BOLD}${DISTRO_NAME}${RESET}
|
||||
Mode: ${BOLD}${MODE}${RESET}
|
||||
|
||||
HEADER
|
||||
|
||||
if [ "$MODE" = "uninstall" ]; then
|
||||
do_uninstall
|
||||
exit 0
|
||||
fi
|
||||
|
||||
extract_payload
|
||||
|
||||
case "$MODE" in
|
||||
cli)
|
||||
install_cli ;;
|
||||
gui)
|
||||
install_qt6
|
||||
install_gui ;;
|
||||
full)
|
||||
install_qt6
|
||||
install_cli
|
||||
install_gui ;;
|
||||
appimage)
|
||||
install_appimage ;;
|
||||
esac
|
||||
|
||||
echo
|
||||
step "Installation complete"
|
||||
case "$MODE" in
|
||||
cli|full) echo " Run: zupt help" ;;
|
||||
esac
|
||||
case "$MODE" in
|
||||
gui|full) echo " Run: zupt-gui (or find 'Zupt GUI' in your applications menu)" ;;
|
||||
esac
|
||||
case "$MODE" in
|
||||
appimage) echo " Run: ./zupt-portable/zupt help" ;;
|
||||
esac
|
||||
echo
|
||||
|
||||
exit 0
|
||||
Loading…
Reference in a new issue