release: restore ZUPT and harden source-only 5.2.2

This commit is contained in:
Cristian Cezar Moisés 2026-08-31 14:14:36 -03:00
commit ff99770bd0
205 changed files with 19627 additions and 13215 deletions

View file

@ -1,56 +1,60 @@
# Maintainer: Cristian Cezar Moisés <sac@securityops.co>
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# AUR submission instructions:
# 1. Run `make dist` upstream to produce zupt-VERSION.tar.gz.
# 2. Upload that tarball somewhere stable (GitHub release / git.securityops.co).
# 2. Upload that tarball to the canonical GitHub release.
# 3. Update `source=()` URL and `sha256sums=()` below.
# 4. Run `makepkg --printsrcinfo > .SRCINFO` in this directory.
# 5. Commit and push to ssh://aur@aur.archlinux.org/zupt.git
# 5. Commit and push to the separately maintained AUR package repository.
#
# Test locally: `makepkg -s` in this directory after dropping a copy of the
# zupt-VERSION.tar.gz alongside the PKGBUILD.
# Test locally with `makepkg -s` after the release archive is published.
pkgname=vaptvupt
pkgver=5.0.0
pkgname=zupt
pkgver=5.2.2
pkgrel=1
provides=('zupt')
replaces=('zupt')
conflicts=('zupt')
pkgdesc='Pure-C11 post-quantum backup compression utility (AES-256-CTR + HMAC-SHA256 + ML-KEM-768 + X25519)'
arch=('x86_64' 'aarch64')
url='https://git.securityops.co/cristiancmoises/vaptvupt'
license=('AGPL-3.0-or-later')
arch=('x86_64')
url='https://github.com/cristiancmoises/zupt'
license=('AGPL-3.0-or-later AND GPL-3.0-or-later AND BSD-2-Clause AND BSD-3-Clause AND CC0-1.0')
depends=('glibc')
makedepends=('gcc')
makedepends=('gcc' 'git' 'make')
checkdepends=('python')
# Replace SHA256 placeholder with output of:
# sha256sum /tmp/zupt-2.4.4.tar.gz
source=("${pkgname}-${pkgver}.tar.gz::https://git.securityops.co/cristiancmoises/vaptvupt/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
sha256sums=('SKIP')
source=("${pkgname}-${pkgver}.tar.gz::https://github.com/cristiancmoises/zupt/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
# Updated from the byte-reproducible upstream release archive before publishing.
sha256sums=('REPLACE_AFTER_FINAL_RELEASE_ARCHIVE_IS_BUILT')
build() {
cd "${pkgname}-${pkgver}"
# Source-only build (WITH_SDK=0) with the project's strict warning set.
CFLAGS="${CFLAGS:--O2 -std=c11} -Wall -Wextra -Wpedantic" \
make WITH_SDK=0 -j"$(nproc)"
make WITH_SDK=0 WITH_PQBOX=0 -j"$(nproc)"
}
check() {
cd "${pkgname}-${pkgver}"
# Distro-safe regression subset (F-06..F-12, dedup-nonce, NIST/RFC vectors).
make WITH_SDK=0 check
# Distro-safe quick, traversal, integrity, codec and NIST/RFC checks.
make WITH_SDK=0 WITH_PQBOX=0 check
}
package() {
cd "${pkgname}-${pkgver}"
# Source-only build (no vendored libraries); `make install` places the
# binary, the zupt symlink, the man pages and the shell completions.
make DESTDIR="${pkgdir}" PREFIX=/usr WITH_SDK=0 install
# binary, man page and shell completions under the public zupt name.
make DESTDIR="${pkgdir}" PREFIX=/usr WITH_SDK=0 WITH_PQBOX=0 \
INSTALL_LEGACY_ALIAS=0 install
# Docs that aren't part of `make install`
install -Dm644 README.md "${pkgdir}/usr/share/doc/${pkgname}/README.md"
install -Dm644 SECURITY.md "${pkgdir}/usr/share/doc/${pkgname}/SECURITY.md"
install -Dm644 CHANGELOG.md "${pkgdir}/usr/share/doc/${pkgname}/CHANGELOG.md"
install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
install -Dm644 LICENSE-AGPL-3.0 "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE-AGPL-3.0"
install -Dm644 LICENSE-GPL-3.0 "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE-GPL-3.0"
install -Dm644 LICENSE-BSD-2-Clause "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE-BSD-2-Clause"
install -Dm644 LICENSE-BSD-3-Clause "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE-BSD-3-Clause"
install -Dm644 LICENSE-CC0-1.0 "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE-CC0-1.0"
install -Dm644 NOTICE "${pkgdir}/usr/share/licenses/${pkgname}/NOTICE"
install -Dm644 THIRD-PARTY-NOTICES.md \
"${pkgdir}/usr/share/licenses/${pkgname}/THIRD-PARTY-NOTICES.md"
}

View file

@ -1,59 +1,165 @@
#!/bin/bash
#!/usr/bin/env bash
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (c) 2025-2026 Cristian Cezar Moisés
# Build vaptvupt CLI as AppImage (portable single-file binary).
# Includes a legacy `zupt` symlink so AppDir users can invoke either name.
set -e
cd "$(dirname "$0")/.."
VERSION="${VERSION:-4.2.1}"
ARCH="${ARCH:-x86_64}"
PKGNAME="vaptvupt"
LEGACY="zupt"
NAME="$PKGNAME-$VERSION-$ARCH"
OUT="/tmp/${NAME}.AppDir"
set -Eeuo pipefail
rm -rf "$OUT"
mkdir -p "$OUT/usr/bin" "$OUT/usr/share/applications" "$OUT/usr/share/icons/hicolor/256x256/apps"
umask 022
export LC_ALL=C
# Source-only build: the binary links only libc/libm/pthread from the host,
# so the AppDir ships no bundled libraries.
install -m 755 $PKGNAME "$OUT/usr/bin/$PKGNAME"
ln -sf $PKGNAME "$OUT/usr/bin/$LEGACY"
die() {
printf 'FAIL: %s\n' "$*" >&2
exit 1
}
cat > "$OUT/AppRun" <<APPRUN
#!/bin/bash
HERE="\$(dirname "\$(readlink -f "\${0}")")"
export PATH="\$HERE/usr/bin:\$PATH"
exec "\$HERE/usr/bin/$PKGNAME" "\$@"
APPRUN
chmod +x "$OUT/AppRun"
[[ $(uname -s) == Linux ]] || die 'AppImage packages must be built on Linux'
cat > "$OUT/$PKGNAME.desktop" <<DESK
[Desktop Entry]
Name=VaptVupt
Comment=Post-quantum backup compression utility (formerly Zupt)
Exec=$PKGNAME
Terminal=true
Type=Application
Categories=Utility;Archiving;Security;
Icon=$PKGNAME
DESK
cp "$OUT/$PKGNAME.desktop" "$OUT/usr/share/applications/"
repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)
cd -- "$repo_root"
# 1x1 PNG placeholder — replace with a real icon when the brand asset exists
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/$PKGNAME.png"
cp "$OUT/$PKGNAME.png" "$OUT/usr/share/icons/hicolor/256x256/apps/$PKGNAME.png"
header_version=$(sed -n 's/^#define ZUPT_VERSION_STRING "\([^"]*\)".*/\1/p' include/zupt.h)
version=${VERSION:-$header_version}
[[ -n $version && $version == "$header_version" ]] || \
die "VERSION '$version' does not match include/zupt.h '$header_version'"
[[ $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "invalid package version: $version"
if command -v appimagetool >/dev/null 2>&1; then
ARCH=$ARCH appimagetool "$OUT" "/tmp/${NAME}.AppImage" 2>&1 | tail -5
echo "Built: /tmp/${NAME}.AppImage"
case $(uname -m) in
x86_64|amd64) native_arch=x86_64 ;;
aarch64|arm64) native_arch=aarch64 ;;
*) die "unsupported native AppImage architecture: $(uname -m)" ;;
esac
case ${ARCH:-$native_arch} in
x86_64|amd64) arch=x86_64 ;;
aarch64|arm64) arch=aarch64 ;;
*) die "unsupported AppImage architecture: ${ARCH:-$native_arch}" ;;
esac
[[ $arch == "$native_arch" ]] || \
die "ARCH=$arch does not match the native build architecture $native_arch"
appimagetool=${APPIMAGETOOL:-appimagetool}
if [[ $appimagetool == */* ]]; then
[[ -x $appimagetool ]] || die "APPIMAGETOOL is not executable: $appimagetool"
appimagetool=$(cd -- "$(dirname -- "$appimagetool")" && pwd -P)/$(basename -- "$appimagetool")
else
appimagetool=$(command -v -- "$appimagetool" || true)
[[ -n $appimagetool ]] || die 'appimagetool not found; set APPIMAGETOOL to a verified local executable'
fi
runtime_file=${APPIMAGE_RUNTIME_FILE:-}
[[ -n $runtime_file && -s $runtime_file ]] || \
die 'set APPIMAGE_RUNTIME_FILE to a locally verified type-2 runtime (network downloads are not performed)'
runtime_file=$(cd -- "$(dirname -- "$runtime_file")" && pwd -P)/$(basename -- "$runtime_file")
runtime_compliance_file=${APPIMAGE_RUNTIME_COMPLIANCE_FILE:-}
[[ -n $runtime_compliance_file && -s $runtime_compliance_file ]] || \
die 'set APPIMAGE_RUNTIME_COMPLIANCE_FILE to the audited runtime license/source-compliance notice'
runtime_compliance_file=$(cd -- "$(dirname -- "$runtime_compliance_file")" && pwd -P)/$(basename -- "$runtime_compliance_file")
dist_dir=${DIST_DIR:-${TMPDIR:-/tmp}/zupt-release}
mkdir -p -- "$dist_dir"
dist_dir=$(cd -- "$dist_dir" && pwd -P)
output=$dist_dir/zupt-${version}-linux-${arch}.AppImage
[[ ! -e $output ]] || die "refusing to overwrite existing output: $output"
for command_name in make readelf file sha256sum; do
command -v -- "$command_name" >/dev/null 2>&1 || die "required command not found: $command_name"
done
run_checks=${RUN_CHECKS:-1}
[[ $run_checks == 0 || $run_checks == 1 ]] || die 'RUN_CHECKS must be 0 or 1'
if [[ $run_checks == 1 ]]; then
command -v git >/dev/null 2>&1 || die 'git is required when RUN_CHECKS=1'
fi
# Always produce the AppDir tarball as well -- some environments (no FUSE,
# strict execve policies, etc.) cannot run the .AppImage directly. The
# tarball is the universal fallback: extract and run AppRun.
cd /tmp
tar -czf "${NAME}.AppDir.tar.gz" "$(basename "$OUT")"
echo "Built: /tmp/${NAME}.AppDir.tar.gz"
echo "Users can run: tar xzf ${NAME}.AppDir.tar.gz && ./${NAME}.AppDir/AppRun version"
jobs=${JOBS:-$(getconf _NPROCESSORS_ONLN 2>/dev/null || printf '1')}
work=$(mktemp -d "${TMPDIR:-/tmp}/zupt-appimage.XXXXXXXX")
appdir=$work/ZUPT.AppDir
image_tmp=$work/$(basename -- "$output")
cleanup() {
make -C "$repo_root" clean >/dev/null 2>&1 || true
chmod -R u+rwX "$work" 2>/dev/null || true
rm -rf -- "$work"
}
trap cleanup EXIT HUP INT TERM
printf '[AppImage] source-only build of ZUPT %s (%s)\n' "$version" "$arch"
make clean
make -j"$jobs" V=1 WITH_SDK=0 WITH_PQBOX=0 INSTALL_LEGACY_ALIAS=0
if [[ $run_checks == 1 ]]; then
make V=1 WITH_SDK=0 WITH_PQBOX=0 INSTALL_LEGACY_ALIAS=0 check
fi
make DESTDIR="$appdir" PREFIX=/usr WITH_SDK=0 WITH_PQBOX=0 \
INSTALL_LEGACY_ALIAS=0 install
binary=$appdir/usr/bin/zupt
[[ -x $binary ]] || die 'AppDir executable is missing'
[[ ! -e $appdir/usr/bin/vaptvupt ]] || die 'legacy vaptvupt alias must not be packaged'
if readelf -d "$binary" 2>/dev/null | grep -Eq '(RPATH|RUNPATH)'; then
readelf -d "$binary" | grep -E '(RPATH|RUNPATH)' >&2
die 'AppDir executable contains RPATH/RUNPATH'
fi
if readelf -d "$binary" 2>/dev/null | grep -Eqi '(vendor/|libvuptsdk|libpqvaptvupt)'; then
die 'AppDir executable references a vendored optional library'
fi
mkdir -p -- "$appdir/usr/share/applications" \
"$appdir/usr/share/doc/zupt" \
"$appdir/usr/share/icons/hicolor/128x128/apps" \
"$appdir/usr/share/licenses/zupt"
install -m 0644 README.md CHANGELOG.md SECURITY.md THREAT_MODEL.md \
"$appdir/usr/share/doc/zupt/"
install -m 0644 LICENSE LICENSE-AGPL-3.0 LICENSE-GPL-3.0 \
LICENSE-BSD-2-Clause LICENSE-BSD-3-Clause LICENSE-CC0-1.0 NOTICE \
THIRD-PARTY-NOTICES.md "$appdir/usr/share/licenses/zupt/"
install -m 0644 gui/LICENSE-GUI \
"$appdir/usr/share/licenses/zupt/GUI-LICENSE.txt"
install -m 0644 gui/assets/README.md \
"$appdir/usr/share/licenses/zupt/GUI-ASSET-PROVENANCE.md"
install -m 0644 "$runtime_compliance_file" \
"$appdir/usr/share/licenses/zupt/AppImage-runtime-compliance.txt"
install -m 0644 gui/assets/zupt-128.png \
"$appdir/usr/share/icons/hicolor/128x128/apps/zupt.png"
cp -- "$appdir/usr/share/icons/hicolor/128x128/apps/zupt.png" "$appdir/zupt.png"
ln -s -- zupt.png "$appdir/.DirIcon"
desktop_file=dev.zupt.cli.desktop
cat > "$appdir/$desktop_file" <<'EOF'
[Desktop Entry]
Type=Application
Name=ZUPT
Comment=Backup compression with authenticated and post-quantum encryption
Exec=zupt
Icon=zupt
Terminal=true
Categories=Utility;Archiving;
EOF
cp -- "$appdir/$desktop_file" "$appdir/usr/share/applications/$desktop_file"
cat > "$appdir/AppRun" <<'EOF'
#!/bin/sh
set -eu
appdir=$(CDPATH= cd -P "$(dirname "$0")" && pwd -P)
exec "$appdir/usr/bin/zupt" "$@"
EOF
chmod 0755 "$appdir/AppRun"
forbidden=$(find "$appdir" -type f \( \
-name '*.o' -o -name '*.obj' -o -name '*.a' -o -name '*.so' -o \
-name '*.so.*' -o -name '*.dll' -o -name '*.dylib' \
\) -print)
[[ -z $forbidden ]] || {
printf '%s\n' "$forbidden" >&2
die 'compiled library or object found in AppDir'
}
bash scripts/test-installed-zupt.sh "$appdir/AppRun"
export ARCH=$arch
export VERSION=$version
export APPIMAGE_EXTRACT_AND_RUN=1
"$appimagetool" --runtime-file "$runtime_file" "$appdir" "$image_tmp"
chmod 0755 "$image_tmp"
file "$image_tmp" | grep -q 'ELF' || die 'generated AppImage does not have ELF magic'
bash scripts/test-installed-zupt.sh "$image_tmp"
mv -- "$image_tmp" "$output"
sha256sum "$output"
printf 'PASS: built and executed-package-tested %s\n' "$output"

View file

@ -1,134 +1,139 @@
#!/bin/bash
#!/usr/bin/env bash
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (c) 2025-2026 Cristian Cezar Moisés
#
# Build self-contained vaptvupt CLI .deb package.
#
# v3.0.0 rename: the binary is now `vaptvupt`; we install it at
# /usr/bin/vaptvupt and create /usr/bin/zupt → /usr/bin/vaptvupt as
# a legacy symlink for one major version cycle. The package name
# is `vaptvupt` with Provides/Replaces/Conflicts on `zupt` so
# `apt install zupt` still resolves cleanly.
#
# Bundles libzuptsdk.so.2 under /usr/lib/vaptvupt/ so users do NOT
# need to separately install the libzuptsdk package.
set -e
cd "$(dirname "$0")/.."
set -Eeuo pipefail
VERSION="${VERSION:-3.0.0}"
ARCH="${ARCH:-amd64}"
PKGNAME="vaptvupt"
LEGACY="zupt"
umask 022
export LC_ALL=C
PKG="${PKGNAME}_${VERSION}_${ARCH}"
ROOT="/tmp/$PKG"
# Vendored libzuptsdk path (relative to project root)
SDK_LIB="vendor/zuptsdk/libzuptsdk.so.2.0.0"
if [ ! -f "$SDK_LIB" ]; then
echo "ERROR: $SDK_LIB not found. Vendor the libzuptsdk shared object first." >&2
exit 1
fi
PQVV_LIB="vendor/pqvaptvupt/libpqvaptvupt.so.0.6.0"
if [ ! -f "$PQVV_LIB" ]; then
echo "ERROR: $PQVV_LIB not found. Vendor the libpqvaptvupt shared object first." >&2
die() {
printf 'FAIL: %s\n' "$*" >&2
exit 1
}
repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)
cd -- "$repo_root"
header_version=$(sed -n 's/^#define ZUPT_VERSION_STRING "\([^"]*\)".*/\1/p' include/zupt.h)
version=${VERSION:-$header_version}
[[ -n $version && $version == "$header_version" ]] || \
die "VERSION '$version' does not match include/zupt.h '$header_version'"
[[ $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "invalid package version: $version"
native_arch=$(dpkg --print-architecture)
arch=${ARCH:-$native_arch}
[[ $arch == "$native_arch" ]] || \
die "ARCH=$arch does not match the native dpkg architecture $native_arch"
dist_dir=${DIST_DIR:-${TMPDIR:-/tmp}/zupt-release}
mkdir -p -- "$dist_dir"
dist_dir=$(cd -- "$dist_dir" && pwd -P)
output=$dist_dir/zupt_${version}_${arch}.deb
[[ ! -e $output ]] || die "refusing to overwrite existing output: $output"
for command_name in make dpkg dpkg-deb readelf sha256sum; do
command -v -- "$command_name" >/dev/null 2>&1 || die "required command not found: $command_name"
done
run_checks=${RUN_CHECKS:-1}
[[ $run_checks == 0 || $run_checks == 1 ]] || die 'RUN_CHECKS must be 0 or 1'
if [[ $run_checks == 1 ]]; then
command -v git >/dev/null 2>&1 || die 'git is required when RUN_CHECKS=1'
fi
echo "[deb] Building vaptvupt"
make clean >/dev/null 2>&1 || true
make -j"$(nproc)" >/dev/null
jobs=${JOBS:-$(getconf _NPROCESSORS_ONLN 2>/dev/null || printf '1')}
work=$(mktemp -d "${TMPDIR:-/tmp}/zupt-deb.XXXXXXXX")
stage=$work/stage
extract=$work/extract
echo "[deb] Patching rpath -> /usr/lib/$PKGNAME:/usr/lib64/$PKGNAME"
patchelf --set-rpath "/usr/lib/$PKGNAME:/usr/lib64/$PKGNAME" $PKGNAME
cleanup() {
make -C "$repo_root" clean >/dev/null 2>&1 || true
chmod -R u+rwX "$work" 2>/dev/null || true
rm -rf -- "$work"
}
trap cleanup EXIT HUP INT TERM
if ! readelf -d $PKGNAME | grep -q "RUNPATH.*\[/usr/lib/$PKGNAME:/usr/lib64/$PKGNAME\]"; then
echo "ERROR: built $PKGNAME does not have correct RUNPATH" >&2
readelf -d $PKGNAME | grep -E "RPATH|RUNPATH"
exit 1
printf '[deb] source-only build of ZUPT %s (%s)\n' "$version" "$arch"
make clean
make -j"$jobs" V=1 WITH_SDK=0 WITH_PQBOX=0 INSTALL_LEGACY_ALIAS=0
if [[ $run_checks == 1 ]]; then
make V=1 WITH_SDK=0 WITH_PQBOX=0 INSTALL_LEGACY_ALIAS=0 check
fi
rm -rf "$ROOT"
mkdir -p "$ROOT/DEBIAN" \
"$ROOT/usr/bin" \
"$ROOT/usr/lib/$PKGNAME" \
"$ROOT/usr/share/doc/$PKGNAME" \
"$ROOT/usr/share/man/man1" \
"$ROOT/usr/share/bash-completion/completions" \
"$ROOT/usr/share/zsh/site-functions" \
"$ROOT/usr/share/fish/vendor_completions.d"
make DESTDIR="$stage" PREFIX=/usr WITH_SDK=0 WITH_PQBOX=0 \
INSTALL_LEGACY_ALIAS=0 install
# Binary + legacy symlink
install -m 755 $PKGNAME "$ROOT/usr/bin/$PKGNAME"
ln -sf $PKGNAME "$ROOT/usr/bin/$LEGACY"
binary=$stage/usr/bin/zupt
[[ -x $binary ]] || die 'staged /usr/bin/zupt is missing'
[[ ! -e $stage/usr/bin/vaptvupt ]] || die 'legacy /usr/bin/vaptvupt must not be packaged'
# Bundled libzuptsdk
install -m 755 "$SDK_LIB" "$ROOT/usr/lib/$PKGNAME/libzuptsdk.so.2.0.0"
ln -sf libzuptsdk.so.2.0.0 "$ROOT/usr/lib/$PKGNAME/libzuptsdk.so.2"
ln -sf libzuptsdk.so.2.0.0 "$ROOT/usr/lib/$PKGNAME/libzuptsdk.so"
install -m 755 "$PQVV_LIB" "$ROOT/usr/lib/$PKGNAME/libpqvaptvupt.so.0.6.0"
ln -sf libpqvaptvupt.so.0.6.0 "$ROOT/usr/lib/$PKGNAME/libpqvaptvupt.so.0"
ln -sf libpqvaptvupt.so.0.6.0 "$ROOT/usr/lib/$PKGNAME/libpqvaptvupt.so"
# Manpage (gzip-compressed); install + legacy alias
if [ -f doc/vaptvupt.1 ]; then
gzip -9n -c doc/vaptvupt.1 > "$ROOT/usr/share/man/man1/$PKGNAME.1.gz"
ln -sf $PKGNAME.1.gz "$ROOT/usr/share/man/man1/$LEGACY.1.gz"
if readelf -d "$binary" 2>/dev/null | grep -Eq '(RPATH|RUNPATH)'; then
readelf -d "$binary" | grep -E '(RPATH|RUNPATH)' >&2
die 'staged executable contains RPATH/RUNPATH'
fi
if readelf -d "$binary" 2>/dev/null | grep -Eqi '(vendor/|libvuptsdk|libpqvaptvupt)'; then
die 'staged executable references a vendored optional library'
fi
# Shell completions
if [ -f completions/vaptvupt.bash ]; then
install -m 0644 completions/vaptvupt.bash "$ROOT/usr/share/bash-completion/completions/$PKGNAME"
ln -sf $PKGNAME "$ROOT/usr/share/bash-completion/completions/$LEGACY"
fi
if [ -f completions/_vaptvupt ]; then
install -m 0644 completions/_vaptvupt "$ROOT/usr/share/zsh/site-functions/_$PKGNAME"
ln -sf _$PKGNAME "$ROOT/usr/share/zsh/site-functions/_$LEGACY"
fi
if [ -f completions/vaptvupt.fish ]; then
install -m 0644 completions/vaptvupt.fish "$ROOT/usr/share/fish/vendor_completions.d/$PKGNAME.fish"
forbidden=$(find "$stage" -type f \( \
-name '*.o' -o -name '*.obj' -o -name '*.a' -o -name '*.so' -o \
-name '*.so.*' -o -name '*.dll' -o -name '*.dylib' -o -name '*.exe' \
\) -print)
[[ -z $forbidden ]] || {
printf '%s\n' "$forbidden" >&2
die 'compiled library or object found in package staging tree'
}
docdir=$stage/usr/share/doc/zupt
mkdir -p -- "$docdir"
install -m 0644 README.md CHANGELOG.md SECURITY.md "$docdir/"
for document in THREAT_MODEL.md NOTICE THIRD-PARTY-NOTICES.md LICENSE-AGPL-3.0 LICENSE-GPL-3.0 LICENSE-BSD-2-Clause LICENSE-BSD-3-Clause LICENSE-CC0-1.0; do
[[ ! -f $document ]] || install -m 0644 "$document" "$docdir/"
done
install -m 0644 LICENSE "$docdir/copyright"
mkdir -p -- "$work/debian" "$stage/DEBIAN"
if [[ -n ${DEB_DEPENDS:-} ]]; then
depends=$DEB_DEPENDS
else
command -v dpkg-shlibdeps >/dev/null 2>&1 || \
die 'dpkg-shlibdeps is required unless DEB_DEPENDS is explicitly set'
printf 'Source: zupt\nPackage: zupt\n' > "$work/debian/control"
shlib_line=$(cd -- "$work" && dpkg-shlibdeps -O -e"$binary")
depends=${shlib_line#shlibs:Depends=}
[[ -n $depends && $depends != "$shlib_line" ]] || \
die 'dpkg-shlibdeps did not determine runtime dependencies'
fi
# Docs
install -m 0644 README.md "$ROOT/usr/share/doc/$PKGNAME/README.md"
install -m 0644 LICENSE "$ROOT/usr/share/doc/$PKGNAME/copyright"
[ -f SECURITY.md ] && install -m 0644 SECURITY.md "$ROOT/usr/share/doc/$PKGNAME/SECURITY.md"
[ -f CHANGELOG.md ] && install -m 0644 CHANGELOG.md "$ROOT/usr/share/doc/$PKGNAME/CHANGELOG.md"
[ -f THREAT_MODEL.md ] && install -m 0644 THREAT_MODEL.md "$ROOT/usr/share/doc/$PKGNAME/THREAT_MODEL.md"
# DEBIAN/control
INSTALLED_KB=$(du -sk "$ROOT/usr" | awk '{print $1}')
cat > "$ROOT/DEBIAN/control" <<EOF
Package: $PKGNAME
Version: $VERSION
installed_kib=$(du -sk "$stage/usr" | awk '{print $1}')
cat > "$stage/DEBIAN/control" <<EOF
Package: zupt
Version: $version
Section: utils
Priority: optional
Architecture: $ARCH
Provides: $LEGACY (= $VERSION)
Replaces: $LEGACY (<< 3.0.0)
Conflicts: $LEGACY (<< 3.0.0)
Depends: libargon2-1, libssl3 | libssl3t64
Installed-Size: $INSTALLED_KB
Architecture: $arch
Depends: $depends
Installed-Size: $installed_kib
Maintainer: Cristian Cezar Moisés <sac@securityops.co>
Homepage: https://git.securityops.co/cristiancmoises/zupt
Description: Post-quantum backup compression utility (formerly zupt)
VaptVupt (renamed from Zupt in v3.0.0 due to a prior INPI Brasil
trademark on the name) is a pure-C11 backup compression utility
featuring post-quantum hybrid encryption (ML-KEM-768 + X25519,
FIPS 203), AES-256-CTR + HMAC-SHA256 authenticated encryption,
Argon2id KDF (PBKDF2-SHA256 via --kdf pbkdf2), multi-threaded
compression with the VaptVupt LZ + ANS codec 2.48.5, full-disk
backup with sparse-region detection, and end-to-end byte-level
tamper detection on encrypted archives (F-09: 0/1827 silent
accepts).
.
The .zupt archive extension is unchanged; v2.x and v3.0.0
archives are bidirectionally compatible. The legacy /usr/bin/zupt
symlink is preserved for one major version cycle.
Homepage: https://github.com/cristiancmoises/zupt
Description: Backup compression with authenticated and post-quantum encryption
ZUPT creates compressed backup archives with optional password encryption
or ML-KEM-768 and X25519 hybrid key encapsulation. This package is built from
source with the optional libvuptsdk and libpqvaptvupt integrations disabled.
EOF
DEB_OUT="/tmp/${PKGNAME}_${VERSION}_${ARCH}.deb"
dpkg-deb --build --root-owner-group "$ROOT" "$DEB_OUT" >/dev/null
echo "Built: $DEB_OUT ($(du -h "$DEB_OUT" | cut -f1))"
dpkg-deb -I "$DEB_OUT" | sed -n '1,20p'
package_tmp=$work/$(basename -- "$output")
dpkg-deb --build --root-owner-group "$stage" "$package_tmp" >/dev/null
dpkg-deb --info "$package_tmp" >/dev/null
dpkg-deb --contents "$package_tmp" > "$work/contents.txt"
if grep -Eq '(/usr/bin/vaptvupt$|\.(o|obj|a|so|so\.[^/]+|dll|dylib)$)' "$work/contents.txt"; then
cat "$work/contents.txt" >&2
die 'forbidden alias or compiled library/object found in .deb contents'
fi
mkdir -p -- "$extract"
dpkg-deb --extract "$package_tmp" "$extract"
bash scripts/test-installed-zupt.sh "$extract/usr/bin/zupt"
mv -- "$package_tmp" "$output"
sha256sum "$output"
printf 'PASS: built and extracted-package-tested %s\n' "$output"

View file

@ -1,188 +1,228 @@
#!/bin/bash
#!/usr/bin/env bash
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (c) 2025-2026 Cristian Cezar Moisés
#
# Build a macOS .dmg installer for the Zupt CLI.
#
# This script MUST be run on macOS — `hdiutil` is required and only
# ships with macOS. There is no portable way to produce a .dmg from
# Linux that Apple's installer will mount cleanly (libdmg-hfsplus and
# dmg2img exist but produce read-only images that some macOS versions
# reject).
#
# On macOS:
# xcode-select --install # one-time, for clang
# make # build the zupt binary
# VERSION=2.4.7 bash packaging/build-dmg.sh
#
# Produces: /tmp/Zupt-VERSION.dmg with:
# - zupt binary (universal2 if built with -arch x86_64 -arch arm64)
# - libzuptsdk dylib alongside the binary at @loader_path
# - install.command (drag-to-install script)
# - README.md, LICENSE
# - Optional: code-signed and notarized if APPLE_DEV_ID env is set
#
# For Homebrew installation, prefer packaging/homebrew/zupt.rb instead.
# The .dmg is for users who don't want to install Homebrew.
set -e
cd "$(dirname "$0")/.."
set -Eeuo pipefail
VERSION="${VERSION:-2.4.7}"
ARCH="${ARCH:-$(uname -m)}" # x86_64 or arm64
NAME="Zupt-${VERSION}-${ARCH}"
STAGE="/tmp/${NAME}.app/Contents"
umask 022
export LC_ALL=C
# ── Platform check ──
if [ "$(uname)" != "Darwin" ]; then
cat >&2 <<EOF
ERROR: build-dmg.sh must be run on macOS.
The .dmg format requires Apple's hdiutil. On Linux:
- Use the .deb (packaging/build-deb.sh) for Debian/Ubuntu/Mint
- Use the .rpm (packaging/build-rpm.sh) for Fedora/RHEL/openSUSE
- Use the AppImage (packaging/build-appimage.sh) for universal Linux
- Use the Homebrew formula on macOS (packaging/homebrew/zupt.rb)
If you need a macOS .pkg without macOS hardware, GitHub Actions has
macos-14 runners that can produce signed .dmg/.pkg artefacts. See
.github/workflows/ci.yml for the matrix template.
EOF
die() {
printf 'FAIL: %s\n' "$*" >&2
exit 1
}
[[ $(uname -s) == Darwin ]] || die 'DMG packages must be built and tested on macOS'
test_macos_binary() (
set -Eeuo pipefail
local candidate=$1 binary test_root archive_size
if [[ $candidate == */* ]]; then
[[ -x $candidate ]] || die "executable not found: $candidate"
binary=$(cd "$(dirname "$candidate")" && pwd -P)/$(basename "$candidate")
else
binary=$(command -v "$candidate" || true)
[[ -n $binary ]] || die "executable not found on PATH: $candidate"
fi
for command_name in cmp dd diff find grep shasum sort; do
command -v "$command_name" >/dev/null 2>&1 || \
die "required smoke-test command not found: $command_name"
done
test_root=$(mktemp -d "${TMPDIR:-/tmp}/zupt-macos-smoke.XXXXXX")
trap 'chmod -R u+rwX "$test_root" 2>/dev/null || true; rm -rf "$test_root"' \
EXIT HUP INT TERM
mkdir -p "$test_root/input/subdir" "$test_root/output" \
"$test_root/password-output" "$test_root/escape-output" "$test_root/outside"
printf 'ZUPT macOS package smoke test\n' > "$test_root/input/text file.txt"
printf 'conteúdo UTF-8\n' > "$test_root/input/subdir/café-安全.txt"
: > "$test_root/input/empty file"
dd if=/dev/urandom of="$test_root/input/subdir/random.bin" \
bs=4096 count=8 >/dev/null 2>&1
printf 'do-not-overwrite\n' > "$test_root/outside/sentinel"
"$binary" --version > "$test_root/version.log" 2>&1
grep -q '^zupt ' "$test_root/version.log"
"$binary" --help > "$test_root/help.log" 2>&1
grep -q '^Usage:' "$test_root/help.log"
if "$binary" --definitely-invalid-option >/dev/null 2>&1; then
die 'invalid option returned success'
fi
(
cd "$test_root"
"$binary" compress plain.zupt input
"$binary" test plain.zupt
"$binary" extract -o output plain.zupt
)
diff -r "$test_root/input" "$test_root/output/input"
(
cd "$test_root/input"
find . -type f -exec shasum -a 256 {} \; | sort
) > "$test_root/original.sha256"
(
cd "$test_root/output/input"
find . -type f -exec shasum -a 256 {} \; | sort
) > "$test_root/extracted.sha256"
cmp "$test_root/original.sha256" "$test_root/extracted.sha256"
(
cd "$test_root"
"$binary" compress -p 'ZUPT-test-password-2026!' \
password.zupt 'input/text file.txt'
"$binary" test -p 'ZUPT-test-password-2026!' password.zupt
"$binary" extract -p 'ZUPT-test-password-2026!' \
-o password-output password.zupt
)
cmp "$test_root/input/text file.txt" \
"$test_root/password-output/input/text file.txt"
if "$binary" extract -p incorrect-password -o "$test_root/wrong-password" \
"$test_root/password.zupt" >/dev/null 2>&1; then
die 'incorrect password returned success'
fi
archive_size=$(wc -c < "$test_root/plain.zupt")
((archive_size > 32)) || die 'archive unexpectedly small'
dd if="$test_root/plain.zupt" of="$test_root/corrupt.zupt" bs=1 \
count="$((archive_size - 17))" >/dev/null 2>&1
if "$binary" test "$test_root/corrupt.zupt" >/dev/null 2>&1; then
die 'truncated archive returned success'
fi
ln -s "$test_root/outside" "$test_root/escape-output/input"
"$binary" extract -o "$test_root/escape-output" \
"$test_root/plain.zupt" >/dev/null 2>&1 || true
[[ $(<"$test_root/outside/sentinel") == do-not-overwrite ]] || \
die 'extraction overwrote outside sentinel'
[[ ! -e $test_root/outside/text\ file.txt && ! -e $test_root/outside/subdir ]] || \
die 'extraction escaped through a destination symlink'
[[ $(id -u) -ne 0 ]] || die 'macOS package smoke test unexpectedly ran as root'
printf 'PASS: native macOS package functional test suite\n'
)
if [[ ${1:-} == --test-binary ]]; then
(($# == 2)) || die 'usage: build-dmg.sh --test-binary PATH'
test_macos_binary "$2"
exit 0
elif (($# != 0)); then
die 'usage: build-dmg.sh [--test-binary PATH]'
fi
# ── Build zupt (universal binary if possible) ──
echo "[dmg] Building zupt"
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)
cd -- "$repo_root"
header_version=$(sed -n 's/^#define ZUPT_VERSION_STRING "\([^"]*\)".*/\1/p' include/zupt.h)
version=${VERSION:-$header_version}
[[ -n $version && $version == "$header_version" ]] || \
die "VERSION '$version' does not match include/zupt.h '$header_version'"
[[ $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "invalid package version: $version"
native_arch=$(uname -m)
arch=${ARCH:-$native_arch}
[[ $arch == "$native_arch" ]] || \
die "ARCH=$arch does not match the native macOS architecture $native_arch"
dist_dir=${DIST_DIR:-${TMPDIR:-/tmp}/zupt-release}
mkdir -p "$dist_dir"
dist_dir=$(cd "$dist_dir" && pwd -P)
output=$dist_dir/ZUPT-${version}-macOS-${arch}.dmg
[[ ! -e $output ]] || die "refusing to overwrite existing output: $output"
for command_name in make clang hdiutil otool plutil shasum; do
command -v -- "$command_name" >/dev/null 2>&1 || die "required command not found: $command_name"
done
run_checks=${RUN_CHECKS:-1}
[[ $run_checks == 0 || $run_checks == 1 ]] || die 'RUN_CHECKS must be 0 or 1'
if [[ $run_checks == 1 ]]; then
command -v git >/dev/null 2>&1 || die 'git is required when RUN_CHECKS=1'
fi
jobs=${JOBS:-$(sysctl -n hw.ncpu 2>/dev/null || printf '1')}
work=$(mktemp -d "${TMPDIR:-/tmp}/zupt-dmg.XXXXXXXX")
app=$work/ZUPT.app
contents=$app/Contents
dmg_root=$work/dmg-root
dmg_tmp=$work/$(basename "$output")
mkdir -p "$contents/MacOS" "$contents/Resources" "$dmg_root"
cleanup() {
make -C "$repo_root" clean >/dev/null 2>&1 || true
chmod -R u+rwX "$work" 2>/dev/null || true
rm -rf "$work"
}
trap cleanup EXIT HUP INT TERM
printf '[dmg] source-only build of ZUPT %s (%s)\n' "$version" "$arch"
make clean
if xcrun --sdk macosx clang -dM -E - </dev/null | grep -q __aarch64__; then
# arm64 host → can cross-build for x86_64 via -arch flag
CFLAGS="-O2 -std=c11 -arch arm64 -arch x86_64" \
LDFLAGS="-arch arm64 -arch x86_64" \
make -j"$(sysctl -n hw.ncpu)" || make -j"$(sysctl -n hw.ncpu)"
else
make -j"$(sysctl -n hw.ncpu)"
make -j"$jobs" CC=clang V=1 WITH_SDK=0 WITH_PQBOX=0 INSTALL_LEGACY_ALIAS=0
if [[ $run_checks == 1 ]]; then
make CC=clang V=1 WITH_SDK=0 WITH_PQBOX=0 INSTALL_LEGACY_ALIAS=0 check
fi
test_macos_binary "$repo_root/zupt"
install -m 0755 zupt "$contents/MacOS/zupt"
for document in README.md CHANGELOG.md LICENSE LICENSE-AGPL-3.0 LICENSE-GPL-3.0 LICENSE-BSD-2-Clause LICENSE-BSD-3-Clause LICENSE-CC0-1.0 NOTICE THIRD-PARTY-NOTICES.md; do
[[ ! -f $document ]] || install -m 0644 "$document" "$contents/Resources/"
done
if otool -l "$contents/MacOS/zupt" | grep -q 'cmd LC_RPATH'; then
otool -l "$contents/MacOS/zupt" >&2
die 'macOS executable contains LC_RPATH'
fi
if otool -L "$contents/MacOS/zupt" | grep -Eqi \
'(vendor/|libvuptsdk|libpqvaptvupt|/home/|/Users/[^/]+/|/opt/(homebrew|local)/|/usr/local/)'; then
otool -L "$contents/MacOS/zupt" >&2
die 'macOS executable references a build path or vendored optional library'
fi
# ── Stage the .app bundle ──
echo "[dmg] Staging .app bundle"
rm -rf "/tmp/${NAME}.app"
mkdir -p "$STAGE/MacOS" "$STAGE/Resources" "$STAGE/Frameworks"
install -m 755 zupt "$STAGE/MacOS/zupt"
# Vendored libzuptsdk — on macOS it'd be .dylib, but if the vendored
# build is Linux-style .so, ship that and warn. A proper macOS build
# would produce libzuptsdk.2.0.0.dylib.
if [ -f vendor/zuptsdk/libzuptsdk.2.0.0.dylib ]; then
install -m 755 vendor/zuptsdk/libzuptsdk.2.0.0.dylib "$STAGE/Frameworks/"
install_name_tool -id "@loader_path/../Frameworks/libzuptsdk.2.0.0.dylib" \
"$STAGE/Frameworks/libzuptsdk.2.0.0.dylib"
install_name_tool -change "vendor/zuptsdk/libzuptsdk.so.2" \
"@loader_path/../Frameworks/libzuptsdk.2.0.0.dylib" \
"$STAGE/MacOS/zupt"
elif [ -f vendor/zuptsdk/libzuptsdk.so.2.0.0 ]; then
cat >&2 <<EOF
WARNING: vendor/zuptsdk ships .so (Linux), not .dylib (macOS).
The .dmg will include the Linux library which won't load on macOS.
Build libzuptsdk natively on macOS first, or modify the Makefile
to produce .dylib output on Darwin.
EOF
install -m 755 vendor/zuptsdk/libzuptsdk.so.2.0.0 "$STAGE/Frameworks/"
fi
# Info.plist (minimal — zupt is a CLI, so the .app is mostly a wrapper)
cat > "$STAGE/Info.plist" <<PLIST
cat > "$contents/Info.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>co.securityops.zupt</string>
<key>CFBundleName</key>
<string>Zupt</string>
<key>CFBundleDisplayName</key>
<string>Zupt</string>
<key>CFBundleVersion</key>
<string>${VERSION}</string>
<key>CFBundleShortVersionString</key>
<string>${VERSION}</string>
<key>CFBundleExecutable</key>
<string>zupt</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>LSMinimumSystemVersion</key>
<string>11.0</string>
<key>CFBundleIdentifier</key><string>dev.zupt.cli</string>
<key>CFBundleName</key><string>ZUPT</string>
<key>CFBundleDisplayName</key><string>ZUPT</string>
<key>CFBundleExecutable</key><string>zupt</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleVersion</key><string>$version</string>
<key>CFBundleShortVersionString</key><string>$version</string>
</dict>
</plist>
PLIST
EOF
plutil -lint "$contents/Info.plist"
cp README.md "$STAGE/Resources/" 2>/dev/null || true
cp LICENSE "$STAGE/Resources/" 2>/dev/null || true
# ── Drag-to-install command file ──
cat > "/tmp/${NAME}-install.command" <<'INSTALL'
#!/bin/bash
# Drag-installer for Zupt CLI. Copies the binary to /usr/local/bin
# (or the user's ~/bin if /usr/local isn't writable).
set -e
DIR="$(cd "$(dirname "$0")" && pwd)"
APP="$DIR/Zupt.app"
TARGET="/usr/local/bin"
if [ ! -w "$TARGET" ]; then
TARGET="$HOME/bin"
mkdir -p "$TARGET"
echo "Installing to $TARGET (add to PATH if missing)"
fi
cp "$APP/Contents/MacOS/zupt" "$TARGET/zupt"
chmod 755 "$TARGET/zupt"
# Bundle the dylib alongside under a stable path
LIBDIR="/usr/local/lib/zupt"
[ -w /usr/local/lib ] || LIBDIR="$HOME/lib/zupt"
mkdir -p "$LIBDIR"
if [ -d "$APP/Contents/Frameworks" ]; then
cp -P "$APP/Contents/Frameworks"/* "$LIBDIR/" 2>/dev/null || true
fi
echo "Installed: $TARGET/zupt"
"$TARGET/zupt" version
INSTALL
chmod 755 "/tmp/${NAME}-install.command"
# ── Optional: code sign ──
if [ -n "${APPLE_DEV_ID:-}" ]; then
echo "[dmg] Code-signing with Developer ID: $APPLE_DEV_ID"
codesign --force --options runtime --sign "$APPLE_DEV_ID" \
--entitlements packaging/macos/entitlements.plist \
"$STAGE/MacOS/zupt" 2>&1 || echo " (no entitlements file — proceeding unsigned for hardening)"
codesign --force --sign "$APPLE_DEV_ID" "/tmp/${NAME}.app" || true
if [[ -n ${CODESIGN_IDENTITY:-} ]]; then
codesign --force --options runtime --timestamp --sign "$CODESIGN_IDENTITY" "$app"
codesign --verify --deep --strict "$app"
fi
# ── Build .dmg ──
echo "[dmg] Building disk image"
DMG="/tmp/${NAME}.dmg"
rm -f "$DMG"
# Stage a directory tree that becomes the .dmg root
DMGSRC="/tmp/${NAME}-dmgsrc"
rm -rf "$DMGSRC"
mkdir -p "$DMGSRC"
cp -R "/tmp/${NAME}.app" "$DMGSRC/Zupt.app"
cp "/tmp/${NAME}-install.command" "$DMGSRC/Install Zupt.command"
[ -f README.md ] && cp README.md "$DMGSRC/"
[ -f LICENSE ] && cp LICENSE "$DMGSRC/"
hdiutil create -fs HFS+ -srcfolder "$DMGSRC" -volname "Zupt ${VERSION}" \
-format UDZO -ov "$DMG"
# ── Optional: notarize ──
if [ -n "${APPLE_DEV_ID:-}" ] && [ -n "${APPLE_NOTARIZE_KEY:-}" ]; then
echo "[dmg] Submitting for notarization"
xcrun notarytool submit "$DMG" --apple-id "$APPLE_DEV_ID" \
--password "$APPLE_NOTARIZE_KEY" --wait
xcrun stapler staple "$DMG"
cp -R "$app" "$dmg_root/ZUPT.app"
cat > "$dmg_root/Install ZUPT.command" <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail
installer_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
source_binary=$installer_dir/ZUPT.app/Contents/MacOS/zupt
target_dir=/usr/local/bin
if [[ ! -d $target_dir || ! -w $target_dir ]]; then
target_dir=${XDG_BIN_HOME:-$HOME/.local/bin}
mkdir -p "$target_dir"
fi
install -m 0755 "$source_binary" "$target_dir/zupt"
printf 'Installed %s\n' "$target_dir/zupt"
"$target_dir/zupt" --version
EOF
chmod 0755 "$dmg_root/Install ZUPT.command"
for document in README.md CHANGELOG.md LICENSE LICENSE-AGPL-3.0 LICENSE-GPL-3.0 LICENSE-BSD-2-Clause LICENSE-BSD-3-Clause LICENSE-CC0-1.0 NOTICE THIRD-PARTY-NOTICES.md; do
[[ ! -f $document ]] || install -m 0644 "$document" "$dmg_root/"
done
echo ""
echo "Built: $DMG ($(du -h "$DMG" | cut -f1))"
echo "Users mount and drag 'Zupt.app' or double-click 'Install Zupt.command'."
hdiutil create -fs HFS+ -srcfolder "$dmg_root" -volname "ZUPT $version" \
-format UDZO -ov "$dmg_tmp"
hdiutil verify "$dmg_tmp"
mv "$dmg_tmp" "$output"
shasum -a 256 "$output"
printf 'PASS: built and native-binary-tested %s\n' "$output"

View file

@ -1,121 +1,140 @@
#!/bin/bash
#!/usr/bin/env 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.2.0}"
APPDIR="/tmp/vaptvupt-gui.AppDir"
# Build a dependency-light GUI AppImage. The ZUPT CLI is compiled from this
# tree and bundled; Python 3 plus PySide6 or PyQt6 remain host requirements.
rm -rf "$APPDIR"
mkdir -p "$APPDIR/usr/bin" \
"$APPDIR/usr/lib/vaptvupt-gui" \
"$APPDIR/usr/share/applications" \
"$APPDIR/usr/share/icons/hicolor/256x256/apps"
set -Eeuo pipefail
umask 022
export LC_ALL=C
# Python source
install -m 644 gui/src/zupt_gui.py "$APPDIR/usr/lib/vaptvupt-gui/"
die() { printf 'FAIL: %s\n' "$*" >&2; exit 1; }
# Wrapper
cat > "$APPDIR/usr/bin/vaptvupt-gui" <<'WRAP'
[[ $(uname -s) == Linux ]] || die 'AppImage packages must be built on Linux'
repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)
cd -- "$repo_root"
header_version=$(sed -n 's/^#define ZUPT_VERSION_STRING "\([^"]*\)".*/\1/p' include/zupt.h)
version=${VERSION:-$header_version}
[[ $version == "$header_version" && $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || \
die "VERSION '$version' does not match include/zupt.h '$header_version'"
case $(uname -m) in
x86_64|amd64) native_arch=x86_64 ;;
aarch64|arm64) native_arch=aarch64 ;;
*) die "unsupported native AppImage architecture: $(uname -m)" ;;
esac
case ${ARCH:-$native_arch} in
x86_64|amd64) arch=x86_64 ;;
aarch64|arm64) arch=aarch64 ;;
*) die "unsupported AppImage architecture: ${ARCH:-$native_arch}" ;;
esac
[[ $arch == "$native_arch" ]] || \
die "ARCH=$arch does not match the native build architecture $native_arch"
dist_dir=${DIST_DIR:-${TMPDIR:-/tmp}/zupt-release}
mkdir -p -- "$dist_dir"
dist_dir=$(cd -- "$dist_dir" && pwd -P)
case $dist_dir/ in "$repo_root"/*) die 'DIST_DIR must be outside the repository' ;; esac
output=$dist_dir/ZUPT-GUI-$version-linux-$arch.AppImage
[[ ! -e $output ]] || die "refusing to overwrite existing output: $output"
appimagetool=${APPIMAGETOOL:-appimagetool}
appimagetool=$(command -v -- "$appimagetool" 2>/dev/null || true)
[[ -n $appimagetool ]] || die 'appimagetool not found; no network fallback is performed'
runtime_file=${APPIMAGE_RUNTIME_FILE:-}
[[ -n $runtime_file && -s $runtime_file ]] || \
die 'set APPIMAGE_RUNTIME_FILE to a non-empty verified local type-2 runtime'
runtime_file=$(cd -- "$(dirname -- "$runtime_file")" && pwd -P)/$(basename -- "$runtime_file")
runtime_compliance_file=${APPIMAGE_RUNTIME_COMPLIANCE_FILE:-}
[[ -n $runtime_compliance_file && -s $runtime_compliance_file ]] || \
die 'set APPIMAGE_RUNTIME_COMPLIANCE_FILE to the audited runtime license/source-compliance notice'
runtime_compliance_file=$(cd -- "$(dirname -- "$runtime_compliance_file")" && pwd -P)/$(basename -- "$runtime_compliance_file")
for command_name in make python3 readelf file sha256sum; do
command -v -- "$command_name" >/dev/null 2>&1 || \
die "required command not found: $command_name"
done
python3 -c 'import PySide6.QtWidgets' 2>/dev/null || \
python3 -c 'import PyQt6.QtWidgets' 2>/dev/null || \
die 'the build/test host needs PySide6 or PyQt6; the AppImage does not download it'
jobs=${JOBS:-$(getconf _NPROCESSORS_ONLN 2>/dev/null || printf '1')}
work=$(mktemp -d "${TMPDIR:-/tmp}/zupt-gui-appimage.XXXXXXXX")
appdir=$work/ZUPT-GUI.AppDir
image_tmp=$work/$(basename -- "$output")
cleanup() {
make -C "$repo_root" clean >/dev/null 2>&1 || true
chmod -R u+rwX "$work" 2>/dev/null || true
rm -rf -- "$work"
}
trap cleanup EXIT HUP INT TERM
make clean
make -j"$jobs" V=1 WITH_SDK=0 WITH_PQBOX=0 INSTALL_LEGACY_ALIAS=0
make V=1 WITH_SDK=0 WITH_PQBOX=0 INSTALL_LEGACY_ALIAS=0 check
make DESTDIR="$appdir" PREFIX=/usr WITH_SDK=0 WITH_PQBOX=0 \
INSTALL_LEGACY_ALIAS=0 install
binary=$appdir/usr/bin/zupt
[[ -x $binary ]] || die 'source-built CLI is missing from AppDir'
[[ ! -e $appdir/usr/bin/vaptvupt ]] || die 'legacy vaptvupt alias must not be packaged'
if readelf -d "$binary" 2>/dev/null | grep -Eq '(RPATH|RUNPATH|libvuptsdk|libpqvaptvupt|vendor/)'; then
readelf -d "$binary" >&2
die 'CLI has RPATH/RUNPATH or an optional-library reference'
fi
install -Dm0644 gui/src/zupt_gui.py "$appdir/usr/lib/zupt-gui/zupt_gui.py"
install -Dm0644 gui/assets/zupt-icon.png \
"$appdir/usr/share/icons/hicolor/256x256/apps/zupt-gui.png"
install -Dm0644 gui/packaging/zupt-gui.desktop \
"$appdir/usr/share/applications/zupt-gui.desktop"
install -d "$appdir/usr/share/licenses/zupt"
install -m 0644 LICENSE LICENSE-AGPL-3.0 LICENSE-GPL-3.0 \
LICENSE-BSD-2-Clause LICENSE-BSD-3-Clause LICENSE-CC0-1.0 NOTICE \
THIRD-PARTY-NOTICES.md "$appdir/usr/share/licenses/zupt/"
install -d "$appdir/usr/share/licenses/zupt-gui"
install -m 0644 LICENSE-AGPL-3.0 \
"$appdir/usr/share/licenses/zupt-gui/LICENSE-AGPL-3.0"
install -m 0644 gui/LICENSE-GUI \
"$appdir/usr/share/licenses/zupt-gui/LICENSE-GUI"
install -m 0644 gui/assets/README.md \
"$appdir/usr/share/licenses/zupt-gui/ASSET-PROVENANCE.md"
install -Dm0644 "$runtime_compliance_file" \
"$appdir/usr/share/licenses/zupt/AppImage-runtime-compliance.txt"
cp -- "$appdir/usr/share/icons/hicolor/256x256/apps/zupt-gui.png" \
"$appdir/zupt-gui.png"
cat >"$appdir/usr/bin/zupt-gui" <<'WRAP'
#!/bin/sh
exec python3 "$(dirname "$0")/../lib/vaptvupt-gui/zupt_gui.py" "$@"
here=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd -P)
export ZUPT_BIN=$here/bin/zupt
exec python3 "$here/lib/zupt-gui/zupt_gui.py" "$@"
WRAP
chmod 755 "$APPDIR/usr/bin/vaptvupt-gui"
# Desktop file
cat > "$APPDIR/vaptvupt-gui.desktop" <<'DESKTOP'
[Desktop Entry]
Type=Application
Name=VaptVupt GUI
GenericName=Backup and Compression Utility
Comment=Post-quantum backup with HKDF combiner, key commitment, HPKE binding
Exec=vaptvupt-gui %f
Icon=vaptvupt-gui
Terminal=false
Categories=Utility;Archiving;Compression;Security;
StartupNotify=true
DESKTOP
cp "$APPDIR/vaptvupt-gui.desktop" "$APPDIR/usr/share/applications/"
# Icon
if [ -f gui/assets/zupt-icon.png ]; then
cp gui/assets/zupt-icon.png "$APPDIR/vaptvupt-gui.png"
cp gui/assets/zupt-icon.png "$APPDIR/usr/share/icons/hicolor/256x256/apps/vaptvupt-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/vaptvupt-gui.png" "$APPDIR/usr/share/icons/hicolor/256x256/apps/vaptvupt-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'
chmod 0755 "$appdir/usr/bin/zupt-gui"
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
vaptvupt-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
vaptvupt-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 vaptvupt >/dev/null 2>&1 && ! command -v zupt >/dev/null 2>&1; then
cat >&2 <<EOF
vaptvupt-gui: warning — neither 'vaptvupt' nor legacy 'zupt' CLI was found in PATH.
Install the vaptvupt package or place the binary in PATH.
The GUI will start but compress/extract operations will fail.
EOF
fi
exec "$HERE/usr/bin/vaptvupt-gui" "$@"
appdir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
exec "$appdir/usr/bin/zupt-gui" "$@"
APPRUN
chmod 755 "$APPDIR/AppRun"
chmod 0755 "$appdir/AppRun"
cp -- "$appdir/usr/share/applications/zupt-gui.desktop" "$appdir/"
# Build AppImage
if command -v appimagetool >/dev/null 2>&1; then
ARCH=x86_64 appimagetool "$APPDIR" "/tmp/VaptVupt-GUI-$VERSION-x86_64.AppImage" 2>&1 | tail -5
echo "Built: /tmp/VaptVupt-GUI-$VERSION-x86_64.AppImage"
else
cd /tmp
rm -f "VaptVupt-GUI-$VERSION-x86_64.AppDir.tar.gz"
tar -czf "VaptVupt-GUI-$VERSION-x86_64.AppDir.tar.gz" vaptvupt-gui.AppDir
cd - >/dev/null
echo "appimagetool unavailable; portable AppDir tarball at:"
echo " /tmp/VaptVupt-GUI-$VERSION-x86_64.AppDir.tar.gz"
echo "Run via: tar -xzf ... && ./vaptvupt-gui.AppDir/AppRun"
echo "Convert to AppImage on a host with appimagetool:"
echo " ARCH=x86_64 appimagetool vaptvupt-gui.AppDir VaptVupt-GUI-$VERSION-x86_64.AppImage"
fi
forbidden=$(find "$appdir" -type f \( \
-name '*.o' -o -name '*.obj' -o -name '*.a' -o -name '*.so' -o \
-name '*.so.*' -o -name '*.dll' -o -name '*.dylib' \
\) -print)
[[ -z $forbidden ]] || { printf '%s\n' "$forbidden" >&2; die 'compiled library/object in AppDir'; }
QT_QPA_PLATFORM=offscreen "$appdir/AppRun" --version | grep -Fq "zupt-gui $version" || \
die 'AppDir GUI/CLI integration check failed'
export ARCH=$arch APPIMAGE_EXTRACT_AND_RUN=1
"$appimagetool" --runtime-file "$runtime_file" "$appdir" "$image_tmp"
chmod 0755 "$image_tmp"
file "$image_tmp" | grep -q ELF || die 'generated AppImage does not have ELF magic'
QT_QPA_PLATFORM=offscreen "$image_tmp" --version | grep -Fq "zupt-gui $version" || \
die 'generated AppImage execution check failed'
mv -- "$image_tmp" "$output"
sha256sum "$output"
printf 'PASS: built and execution-tested %s\n' "$output"

View file

@ -1,181 +1,113 @@
#!/bin/bash
#!/usr/bin/env 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.2.0}"
ARCH="all"
PKG="vaptvupt-gui_${VERSION}_${ARCH}"
ROOT="/tmp/$PKG"
# Build the architecture-independent GUI package from tracked source. The CLI
# dependency is built and tested in baseline mode but is packaged separately.
rm -rf "$ROOT"
mkdir -p "$ROOT/DEBIAN" \
"$ROOT/usr/bin" \
"$ROOT/usr/lib/vaptvupt-gui" \
"$ROOT/usr/share/applications" \
"$ROOT/usr/share/icons/hicolor/256x256/apps" \
"$ROOT/usr/share/man/man1" \
"$ROOT/usr/share/doc/vaptvupt-gui"
set -Eeuo pipefail
umask 022
export LC_ALL=C
# Source files
install -m 644 gui/src/zupt_gui.py "$ROOT/usr/lib/vaptvupt-gui/"
die() { printf 'FAIL: %s\n' "$*" >&2; exit 1; }
# Wrapper script in /usr/bin
cat > "$ROOT/usr/bin/vaptvupt-gui" <<'WRAP'
#!/bin/sh
exec python3 /usr/lib/vaptvupt-gui/zupt_gui.py "$@"
WRAP
chmod 755 "$ROOT/usr/bin/vaptvupt-gui"
# v3.0.0: legacy zupt-gui symlink
ln -sf vaptvupt-gui "$ROOT/usr/bin/zupt-gui"
repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)
cd -- "$repo_root"
# Desktop entry
cat > "$ROOT/usr/share/applications/vaptvupt-gui.desktop" <<'DESKTOP'
[Desktop Entry]
Type=Application
Name=VaptVupt GUI
GenericName=Backup and Compression Utility
Comment=Post-quantum backup with HKDF combiner, key commitment, HPKE binding
Exec=vaptvupt-gui %f
Icon=vaptvupt-gui
Terminal=false
Categories=Utility;Archiving;Compression;Security;
StartupNotify=true
MimeType=application/x-zupt;
Keywords=archive;compression;encryption;post-quantum;backup;
DESKTOP
header_version=$(sed -n 's/^#define ZUPT_VERSION_STRING "\([^"]*\)".*/\1/p' include/zupt.h)
version=${VERSION:-$header_version}
[[ $version == "$header_version" && $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || \
die "VERSION '$version' does not match include/zupt.h '$header_version'"
# Man page
if [ -f doc/vaptvupt-gui.1 ]; then
install -m 644 doc/vaptvupt-gui.1 "$ROOT/usr/share/man/man1/vaptvupt-gui.1"
gzip -9n "$ROOT/usr/share/man/man1/vaptvupt-gui.1"
fi
dist_dir=${DIST_DIR:-${TMPDIR:-/tmp}/zupt-release}
mkdir -p -- "$dist_dir"
dist_dir=$(cd -- "$dist_dir" && pwd -P)
case $dist_dir/ in "$repo_root"/*) die 'DIST_DIR must be outside the repository' ;; esac
output=$dist_dir/zupt-gui_${version}_all.deb
[[ ! -e $output ]] || die "refusing to overwrite existing output: $output"
# Icon
if [ -f gui/assets/zupt-icon.png ]; then
cp gui/assets/zupt-icon.png "$ROOT/usr/share/icons/hicolor/256x256/apps/vaptvupt-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
for command_name in make python3 dpkg-deb gzip sha256sum; do
command -v -- "$command_name" >/dev/null 2>&1 || \
die "required command not found: $command_name"
done
# Docs
install -m 644 gui/README.md "$ROOT/usr/share/doc/vaptvupt-gui/" 2>/dev/null || true
gzip -9n -c CHANGELOG.md > "$ROOT/usr/share/doc/vaptvupt-gui/changelog.gz"
jobs=${JOBS:-$(getconf _NPROCESSORS_ONLN 2>/dev/null || printf '1')}
work=$(mktemp -d "${TMPDIR:-/tmp}/zupt-gui-deb.XXXXXXXX")
stage=$work/stage
extract=$work/extract
cleanup() {
make -C "$repo_root" clean >/dev/null 2>&1 || true
chmod -R u+rwX "$work" 2>/dev/null || true
rm -rf -- "$work"
}
trap cleanup EXIT HUP INT TERM
cat > "$ROOT/usr/share/doc/vaptvupt-gui/copyright" <<'COPYRIGHT'
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: vaptvupt-gui
Upstream-Contact: Cristian Cezar Moisés <zupt@riseup.net>
Source: https://git.securityops.co/cristiancmoises/zupt
printf '[GUI deb] validating source-only CLI dependency %s\n' "$version"
make clean
make -j"$jobs" V=1 WITH_SDK=0 WITH_PQBOX=0 INSTALL_LEGACY_ALIAS=0
make V=1 WITH_SDK=0 WITH_PQBOX=0 INSTALL_LEGACY_ALIAS=0 check
./zupt version | grep -Fq "zupt $version" || die 'CLI version check failed'
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
PYTHONDONTWRITEBYTECODE=1 python3 - <<'PY'
from pathlib import Path
source = Path("gui/src/zupt_gui.py").read_text(encoding="utf-8")
compile(source, "gui/src/zupt_gui.py", "exec")
PY
# Control
INSTALLED_SIZE=$(du -sk "$ROOT" | cut -f1)
cat > "$ROOT/DEBIAN/control" <<EOF
Package: vaptvupt-gui
Version: $VERSION
mkdir -p -- "$stage"
bash gui/install.sh --destdir "$stage" --prefix /usr
[[ ! -e $stage/usr/bin/vaptvupt-gui ]] || die 'legacy vaptvupt-gui alias must not be packaged'
install -d -- "$stage/usr/share/doc/zupt-gui" "$stage/DEBIAN"
install -m 0644 -- gui/README.md "$stage/usr/share/doc/zupt-gui/README.md"
gzip -9n -c CHANGELOG.md >"$stage/usr/share/doc/zupt-gui/changelog.gz"
install -m 0644 -- LICENSE-AGPL-3.0 "$stage/usr/share/doc/zupt-gui/copyright"
gzip -9n -- "$stage/usr/share/man/man1/zupt-gui.1"
installed_kib=$(du -sk "$stage/usr" | awk '{print $1}')
cat >"$stage/DEBIAN/control" <<EOF
Package: zupt-gui
Version: $version
Section: utils
Priority: optional
Architecture: $ARCH
Depends: python3 (>= 3.9), python3-pyqt6 | python3-pyside6, vaptvupt (>= 3.0.0) | zupt (>= 2.2.3)
Provides: zupt-gui (= ${VERSION})
Replaces: zupt-gui (<< 1.2.0)
Conflicts: zupt-gui (<< 1.2.0)
Maintainer: Cristian Cezar Moisés <zupt@riseup.net>
Installed-Size: $INSTALLED_SIZE
Homepage: https://git.securityops.co/cristiancmoises/zupt
Description: Graphical interface for VaptVupt post-quantum backup utility
PySide6/PyQt6 frontend for VaptVupt (formerly zupt-gui in 1.x). 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.
Architecture: all
Depends: python3 (>= 3.9), python3-pyqt6 | python3-pyside6.qtwidgets, zupt (= $version)
Installed-Size: $installed_kib
Maintainer: Cristian Cezar Moisés <sac@securityops.co>
Homepage: https://github.com/cristiancmoises/zupt
Description: Qt graphical interface for the ZUPT backup utility
The GUI creates, inspects, verifies, and extracts .zupt archives through the
separately packaged zupt command. Optional SDK and PQ-box controls are
shown only when the installed command reports those integrations enabled.
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
forbidden=$(find "$stage" -type f \( \
-name '*.o' -o -name '*.obj' -o -name '*.a' -o -name '*.so' -o \
-name '*.so.*' -o -name '*.dll' -o -name '*.dylib' -o -name '*.exe' \
\) -print)
[[ -z $forbidden ]] || { printf '%s\n' "$forbidden" >&2; die 'compiled artifact in GUI package'; }
package_tmp=$work/$(basename -- "$output")
source_epoch=${SOURCE_DATE_EPOCH:-$(sed -n '1p' .source-date-epoch 2>/dev/null || true)}
[[ $source_epoch =~ ^[0-9]+$ ]] || die 'SOURCE_DATE_EPOCH is not available'
SOURCE_DATE_EPOCH=$source_epoch dpkg-deb -Zxz --build --root-owner-group \
"$stage" "$package_tmp" >/dev/null
dpkg-deb --info "$package_tmp" >/dev/null
dpkg-deb --contents "$package_tmp" >"$work/contents.txt"
grep -q './usr/bin/zupt-gui' "$work/contents.txt" || die 'GUI launcher missing from .deb'
if grep -Eq '(/usr/bin/vaptvupt-gui|\.(o|obj|a|so|so\.[^/]+|dll|dylib|exe)$)' "$work/contents.txt"; then
cat "$work/contents.txt" >&2
die 'forbidden compatibility alias or compiled artifact in .deb'
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'
mkdir -p -- "$extract"
dpkg-deb --extract "$package_tmp" "$extract"
PYTHONDONTWRITEBYTECODE=1 python3 - <<PY
from pathlib import Path
p = Path("$extract/usr/lib/zupt-gui/zupt_gui.py")
compile(p.read_text(encoding="utf-8"), str(p), "exec")
PY
──────────────────────────────────────────────────────────────────────
vaptvupt-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: vaptvupt-gui
──────────────────────────────────────────────────────────────────────
MSG
fi
# Same friendly warning if zupt CLI not installed.
if ! command -v vaptvupt >/dev/null 2>&1 && ! command -v zupt >/dev/null 2>&1; then
cat << 'MSG'
──────────────────────────────────────────────────────────────────────
vaptvupt-gui needs the 'vaptvupt' CLI to function. Install it:
Debian/Ubuntu/Mint: sudo dpkg -i vaptvupt_3.0.0_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
mv -- "$package_tmp" "$output"
sha256sum "$output"
printf 'PASS: built and content-validated %s\n' "$output"

View file

@ -1,151 +1,146 @@
#!/bin/bash
#!/usr/bin/env 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.2.0}"
RPMROOT="/tmp/rpmbuild-vaptvupt-gui"
rm -rf "$RPMROOT"
mkdir -p "$RPMROOT"/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
# Build a real noarch RPM and source RPM. Run this in a native RPM build
# environment; there is deliberately no --nodeps or tarball fallback.
TMP="/tmp/vaptvupt-gui-$VERSION"
rm -rf "$TMP" && mkdir -p "$TMP/src" "$TMP/doc" "$TMP/assets"
cp gui/src/zupt_gui.py "$TMP/src/"
cp doc/vaptvupt-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/vaptvupt-gui-$VERSION.tar.gz" -C /tmp "vaptvupt-gui-$VERSION"
set -Eeuo pipefail
umask 022
export LC_ALL=C
cat > "$RPMROOT/SPECS/vaptvupt-gui.spec" <<EOF
Name: vaptvupt-gui
Version: $VERSION
Release: 1%{?dist}
Summary: Graphical interface for VaptVupt post-quantum backup utility (formerly zupt-gui)
die() { printf 'FAIL: %s\n' "$*" >&2; exit 1; }
repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)
cd -- "$repo_root"
header_version=$(sed -n 's/^#define ZUPT_VERSION_STRING "\([^"]*\)".*/\1/p' include/zupt.h)
version=${VERSION:-$header_version}
[[ $version == "$header_version" && $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || \
die "VERSION '$version' does not match include/zupt.h '$header_version'"
dist_dir=${DIST_DIR:-${TMPDIR:-/tmp}/zupt-release}
mkdir -p -- "$dist_dir"
dist_dir=$(cd -- "$dist_dir" && pwd -P)
case $dist_dir/ in "$repo_root"/*) die 'DIST_DIR must be outside the repository' ;; esac
for command_name in make python3 rpmbuild rpm rpm2cpio cpio tar sha256sum; do
command -v -- "$command_name" >/dev/null 2>&1 || \
die "required command not found: $command_name"
done
jobs=${JOBS:-$(getconf _NPROCESSORS_ONLN 2>/dev/null || printf '1')}
work=$(mktemp -d "${TMPDIR:-/tmp}/zupt-gui-rpm.XXXXXXXX")
top=$work/rpmbuild
tree=$work/zupt-gui-$version
extract=$work/extract
mkdir -p -- "$top"/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS} \
"$tree"/{src,assets,doc} "$extract"
cleanup() {
make -C "$repo_root" clean >/dev/null 2>&1 || true
chmod -R u+rwX "$work" 2>/dev/null || true
rm -rf -- "$work"
}
trap cleanup EXIT HUP INT TERM
printf '[GUI rpm] validating source-only CLI dependency %s\n' "$version"
make clean
make -j"$jobs" V=1 WITH_SDK=0 WITH_PQBOX=0 INSTALL_LEGACY_ALIAS=0
make V=1 WITH_SDK=0 WITH_PQBOX=0 INSTALL_LEGACY_ALIAS=0 check
./zupt version | grep -Fq "zupt $version" || die 'CLI version check failed'
install -m 0644 gui/src/zupt_gui.py "$tree/src/"
install -m 0644 gui/assets/zupt-icon.png "$tree/assets/"
install -m 0644 gui/packaging/zupt-gui.desktop "$tree/"
install -m 0644 doc/zupt-gui.1 "$tree/doc/"
install -m 0644 gui/README.md "$tree/README.md"
install -m 0644 LICENSE LICENSE-AGPL-3.0 "$tree/"
install -m 0644 gui/LICENSE-GUI "$tree/LICENSE-GUI"
install -m 0644 gui/assets/README.md "$tree/ASSET-PROVENANCE.md"
source_epoch=${SOURCE_DATE_EPOCH:-$(sed -n '1p' .source-date-epoch 2>/dev/null || true)}
[[ $source_epoch =~ ^[0-9]+$ ]] || die 'SOURCE_DATE_EPOCH is not available'
source_tar=$top/SOURCES/zupt-gui-$version.tar.gz
tar --sort=name --mtime="@$source_epoch" --owner=0 --group=0 --numeric-owner \
-czf "$source_tar" -C "$work" "zupt-gui-$version"
cat >"$top/SPECS/zupt-gui.spec" <<EOF
Name: zupt-gui
Version: $version
Release: 1
Summary: Qt graphical interface for the ZUPT backup utility
License: AGPL-3.0-or-later
URL: https://git.securityops.co/cristiancmoises/zupt
Source0: vaptvupt-gui-%{version}.tar.gz
URL: https://github.com/cristiancmoises/zupt
Source0: %{name}-%{version}.tar.gz
BuildArch: noarch
BuildRequires: python3 >= 3.9
Requires: python3 >= 3.9
Requires: (python3-qt6 or python3-pyside6 or python3-pyqt6)
Requires: (vaptvupt >= 3.0.0 or zupt >= 2.2.3)
Provides: zupt-gui = %{version}-%{release}
Obsoletes: zupt-gui < 1.2.0
Conflicts: zupt-gui < 1.2.0
Requires: zupt >= %{version}
%description
PySide6/PyQt6 frontend for VaptVupt (renamed from zupt-gui in 1.x). 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.
ZUPT GUI creates, inspects, verifies, and extracts .zupt archives through
the separately packaged zupt command. Optional SDK and PQ-box controls are
shown only when that command reports the corresponding integration enabled.
%prep
%autosetup
%build
# nothing to build; pure Python
%check
python3 -c 'from pathlib import Path; p=Path("src/zupt_gui.py"); compile(p.read_text(encoding="utf-8"), str(p), "exec")'
%install
install -Dm0644 src/zupt_gui.py %{buildroot}%{_datadir}/zupt-gui/zupt_gui.py
install -Dm0644 zupt-gui.desktop %{buildroot}%{_datadir}/applications/zupt-gui.desktop
install -Dm0644 assets/zupt-icon.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/zupt-gui.png
install -Dm0644 doc/zupt-gui.1 %{buildroot}%{_mandir}/man1/zupt-gui.1
mkdir -p %{buildroot}%{_bindir}
mkdir -p %{buildroot}%{_libdir}/vaptvupt-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}/vaptvupt-gui/
cat > %{buildroot}%{_bindir}/vaptvupt-gui <<'WRAP'
cat >%{buildroot}%{_bindir}/zupt-gui <<'WRAP'
#!/bin/sh
exec python3 %{_libdir}/vaptvupt-gui/zupt_gui.py "\$@"
exec python3 %{_datadir}/zupt-gui/zupt_gui.py "\$@"
WRAP
chmod 755 %{buildroot}%{_bindir}/vaptvupt-gui
# v3.0.0: legacy zupt-gui symlink for one major version cycle
ln -sf vaptvupt-gui %{buildroot}%{_bindir}/zupt-gui
cat > %{buildroot}%{_datadir}/applications/vaptvupt-gui.desktop <<'DESKTOP'
[Desktop Entry]
Type=Application
Name=VaptVupt GUI
GenericName=Backup and Compression Utility
Comment=Post-quantum backup with HKDF combiner, key commitment, HPKE binding
Exec=vaptvupt-gui %f
Icon=vaptvupt-gui
Terminal=false
Categories=Utility;Archiving;Compression;Security;
StartupNotify=true
DESKTOP
[ -f doc/vaptvupt-gui.1 ] && install -m 644 doc/vaptvupt-gui.1 %{buildroot}%{_mandir}/man1/
[ -f assets/zupt-icon.png ] && install -m 644 assets/zupt-icon.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/vaptvupt-gui.png || true
# Generate placeholder icon if no real one exists
if [ ! -f %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/vaptvupt-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/vaptvupt-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
chmod 0755 %{buildroot}%{_bindir}/zupt-gui
%files
%doc README.md
%license LICENSE
%{_bindir}/vaptvupt-gui
%license LICENSE LICENSE-AGPL-3.0 LICENSE-GUI
%doc README.md ASSET-PROVENANCE.md
%{_bindir}/zupt-gui
%{_libdir}/vaptvupt-gui/zupt_gui.py
%{_datadir}/applications/vaptvupt-gui.desktop
%{_datadir}/icons/hicolor/256x256/apps/vaptvupt-gui.png
%{_datadir}/zupt-gui/zupt_gui.py
%{_datadir}/applications/zupt-gui.desktop
%{_datadir}/icons/hicolor/256x256/apps/zupt-gui.png
%{_mandir}/man1/zupt-gui.1*
%changelog
* Sun May 25 2026 Cristian Cezar Moisés <zupt@riseup.net> - $VERSION-1
- v1.2.0: package renamed zupt-gui → vaptvupt-gui (parent CLI also
renamed; INPI Brasil trademark on "Zupt"). Legacy /usr/bin/zupt-gui
symlink preserved. GUI binary-discovery bug fix: _find_vaptvupt
with liveness check + discovery log via VAPTVUPT_DEBUG=1.
* Mon Apr 27 2026 Cristian Cezar Moisés <zupt@riseup.net> - 1.1.1-1
- Cross-binding (PySide6 OR PyQt6 auto-detected)
- SDK v2 mode toggles in compress/extract/keygen tabs
- Man page added
* Mon Aug 31 2026 Cristian Cezar Moisés <sac@securityops.co> - $version-1
- Package the integrated GUI under its restored ZUPT identity.
- Require the separately built source-only baseline CLI package.
EOF
if command -v rpmbuild >/dev/null 2>&1; then
# On Debian/Ubuntu, the host's `rpm` doesn't see `python3` as an RPM
# (it's a deb), so the BuildRequires check would fail. Use --nodeps
# since the runtime check on the target system is what actually
# matters. The Requires: lines still apply on install.
rpmbuild --define "_topdir $RPMROOT" --nodeps -bb "$RPMROOT/SPECS/vaptvupt-gui.spec" 2>&1 | tail -3
if [ -f "$RPMROOT/RPMS/noarch/vaptvupt-gui-${VERSION}-1.noarch.rpm" ]; then
cp "$RPMROOT/RPMS/noarch/vaptvupt-gui-${VERSION}-1.noarch.rpm" \
"/tmp/vaptvupt-gui-${VERSION}-1.noarch.rpm"
echo "Built: /tmp/vaptvupt-gui-${VERSION}-1.noarch.rpm"
fi
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"
rpmbuild --define "_topdir $top" -ba "$top/SPECS/zupt-gui.spec"
mapfile -t main_rpms < <(find "$top/RPMS" -type f -name "zupt-gui-$version-*.noarch.rpm" -print | sort)
mapfile -t source_rpms < <(find "$top/SRPMS" -type f -name "zupt-gui-$version-*.src.rpm" -print | sort)
[[ ${#main_rpms[@]} -eq 1 ]] || die "expected one GUI RPM, found ${#main_rpms[@]}"
[[ ${#source_rpms[@]} -eq 1 ]] || die "expected one GUI source RPM, found ${#source_rpms[@]}"
rpm -qpl "${main_rpms[0]}" >"$work/contents.txt"
grep -q '^/usr/bin/zupt-gui$' "$work/contents.txt" || die 'GUI launcher missing from RPM'
if grep -Eq '(^/usr/bin/vaptvupt-gui$|\.(o|obj|a|so|so\.[^/]+|dll|dylib|exe)$)' "$work/contents.txt"; then
cat "$work/contents.txt" >&2
die 'forbidden compatibility alias or compiled artifact in GUI RPM'
fi
(cd -- "$extract" && rpm2cpio "${main_rpms[0]}" | cpio -idm --quiet)
PYTHONDONTWRITEBYTECODE=1 python3 - <<PY
from pathlib import Path
p = Path("$extract/usr/share/zupt-gui/zupt_gui.py")
compile(p.read_text(encoding="utf-8"), str(p), "exec")
PY
for artifact in "${main_rpms[0]}" "${source_rpms[0]}"; do
destination=$dist_dir/$(basename -- "$artifact")
[[ ! -e $destination ]] || die "refusing to overwrite existing output: $destination"
cp -- "$artifact" "$destination"
sha256sum "$destination"
done
printf 'PASS: built and content-validated GUI RPM and source RPM in %s\n' "$dist_dir"

View file

@ -1,339 +0,0 @@
#!/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.3')
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 = f'/tmp/zupt_{VERSION}_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()

View file

@ -1,195 +1,127 @@
#!/bin/bash
#!/usr/bin/env bash
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (c) 2025-2026 Cristian Cezar Moisés
#
# Build self-contained vaptvupt RPM (formerly zupt). Bundles
# libzuptsdk.so.2 under /usr/lib/vaptvupt/ so users do NOT need
# a separate libzuptsdk package. Installs a legacy /usr/bin/zupt
# symlink for one major version cycle.
set -e
cd "$(dirname "$0")/.."
set -Eeuo pipefail
VERSION="${VERSION:-3.0.0}"
ARCH="${ARCH:-x86_64}"
RELEASE="1"
PKGNAME="vaptvupt"
LEGACY="zupt"
umask 022
export LC_ALL=C
SDK_LIB="vendor/zuptsdk/libzuptsdk.so.2.0.0"
if [ ! -f "$SDK_LIB" ]; then
echo "ERROR: $SDK_LIB not found." >&2
die() {
printf 'FAIL: %s\n' "$*" >&2
exit 1
fi
}
echo "[rpm] Building $PKGNAME"
make clean >/dev/null 2>&1 || true
make -j"$(nproc)" >/dev/null
echo "[rpm] Patching rpath -> /usr/lib/$PKGNAME:/usr/lib64/$PKGNAME"
patchelf --set-rpath "/usr/lib/$PKGNAME:/usr/lib64/$PKGNAME" $PKGNAME
if ! readelf -d $PKGNAME | grep -q "RUNPATH.*\[/usr/lib/$PKGNAME:/usr/lib64/$PKGNAME\]"; then
echo "ERROR: $PKGNAME does not have correct RUNPATH" >&2
exit 1
fi
repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)
cd -- "$repo_root"
if ! command -v rpmbuild >/dev/null 2>&1; then
echo "[rpm] rpmbuild not found; install rpm package to proceed"
exit 1
fi
header_version=$(sed -n 's/^#define ZUPT_VERSION_STRING "\([^"]*\)".*/\1/p' include/zupt.h)
version=${VERSION:-$header_version}
[[ -n $version && $version == "$header_version" ]] || \
die "VERSION '$version' does not match include/zupt.h '$header_version'"
[[ $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "invalid package version: $version"
RPMROOT="/tmp/rpmbuild-$PKGNAME"
rm -rf "$RPMROOT"
mkdir -p "$RPMROOT"/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
spec=packaging/opensuse/zupt.spec
[[ -f $spec ]] || die "spec file not found: $spec"
spec_version=$(sed -n 's/^Version:[[:space:]]*//p' "$spec" | head -n 1)
[[ $spec_version == "$version" ]] || die "spec version '$spec_version' does not match '$version'"
STAGE="/tmp/$PKGNAME-rpm-stage-${VERSION}"
rm -rf "$STAGE"
mkdir -p "$STAGE/$PKGNAME-${VERSION}/completions"
dist_dir=${DIST_DIR:-${TMPDIR:-/tmp}/zupt-release}
mkdir -p -- "$dist_dir"
dist_dir=$(cd -- "$dist_dir" && pwd -P)
cp $PKGNAME "$STAGE/$PKGNAME-${VERSION}/$PKGNAME"
cp "$SDK_LIB" "$STAGE/$PKGNAME-${VERSION}/libzuptsdk.so.2.0.0"
cp "vendor/pqvaptvupt/libpqvaptvupt.so.0.6.0" "$STAGE/$PKGNAME-${VERSION}/libpqvaptvupt.so.0.6.0"
cp README.md CHANGELOG.md SECURITY.md AUDIT.md LICENSE "$STAGE/$PKGNAME-${VERSION}/"
[ -f doc/vaptvupt.1 ] && cp doc/vaptvupt.1 "$STAGE/$PKGNAME-${VERSION}/$PKGNAME.1"
[ -f completions/vaptvupt.bash ] && cp completions/vaptvupt.bash "$STAGE/$PKGNAME-${VERSION}/completions/"
[ -f completions/_vaptvupt ] && cp completions/_vaptvupt "$STAGE/$PKGNAME-${VERSION}/completions/"
[ -f completions/vaptvupt.fish ] && cp completions/vaptvupt.fish "$STAGE/$PKGNAME-${VERSION}/completions/"
tar -czf "$RPMROOT/SOURCES/$PKGNAME-${VERSION}.tar.gz" -C "$STAGE" "$PKGNAME-${VERSION}"
for command_name in make git rpmbuild rpm rpm2cpio cpio date readelf sha256sum tar; do
command -v -- "$command_name" >/dev/null 2>&1 || die "required command not found: $command_name"
done
cat > "$RPMROOT/SPECS/$PKGNAME.spec" <<EOF
Name: $PKGNAME
Version: $VERSION
Release: ${RELEASE}%{?dist}
Summary: Post-quantum backup compression utility (formerly zupt)
License: AGPL-3.0-or-later AND GPL-3.0-or-later
URL: https://git.securityops.co/cristiancmoises/zupt
Source0: $PKGNAME-%{version}.tar.gz
work=$(mktemp -d "${TMPDIR:-/tmp}/zupt-rpm.XXXXXXXX")
top=$work/rpmbuild
extract=$work/extract
mkdir -p -- "$top/BUILD" "$top/BUILDROOT" "$top/RPMS" "$top/SOURCES" \
"$top/SPECS" "$top/SRPMS" "$extract"
# v3.0.0 rename — INPI Brasil trademark on the prior name "Zupt".
# Cleanly supersede legacy 'zupt' RPMs.
Provides: $LEGACY = %{version}-%{release}
Obsoletes: $LEGACY < 3.0.0
Conflicts: $LEGACY < 3.0.0
cleanup() {
chmod -R u+rwX "$work" 2>/dev/null || true
rm -rf -- "$work"
}
trap cleanup EXIT HUP INT TERM
Requires: libargon2
Requires: openssl-libs >= 3.0
AutoReqProv: no
source_tar=$top/SOURCES/zupt-${version}.tar.gz
printf '[rpm] creating audited source archive for ZUPT %s\n' "$version"
make DIST_TARBALL="$source_tar" WITH_SDK=0 WITH_PQBOX=0 dist
archive_version=$(tar -xOf "$source_tar" "zupt-${version}/include/zupt.h" | \
sed -n 's/^#define ZUPT_VERSION_STRING "\([^"]*\)".*/\1/p')
[[ $archive_version == "$version" ]] || die "source archive version is '$archive_version', expected '$version'"
%global debug_package %{nil}
%global __os_install_post %{nil}
%global _build_id_links none
install -m 0644 "$spec" "$top/SPECS/zupt.spec"
# OBS converts zupt.changes into RPM changelog metadata. Standalone
# rpmbuild does not, so add an equivalent release entry only to the temporary
# spec used for this release artifact.
changelog_sections=$(grep -Ec '^%changelog[[:space:]]*$' "$top/SPECS/zupt.spec" || true)
[[ $changelog_sections -eq 1 ]] || \
die "expected exactly one %changelog section, found $changelog_sections"
source_epoch=${SOURCE_DATE_EPOCH:-$(sed -n '1p' .source-date-epoch 2>/dev/null || true)}
[[ $source_epoch =~ ^[0-9]+$ ]] || die 'SOURCE_DATE_EPOCH is not available'
changelog_date=$(date -u --date="@$source_epoch" '+%a %b %d %Y')
cat >> "$top/SPECS/zupt.spec" <<EOF
%description
VaptVupt (renamed from Zupt in v3.0.0 due to INPI Brasil trademark
on the prior name) is a backup-oriented compression utility with
hybrid post-quantum encryption (ML-KEM-768 + X25519). Provides
AES-256-CTR + HMAC-SHA256 authenticated encryption, multi-threaded
compression, full-disk backup/restore, block-level deduplication,
and embeds the VaptVupt 2.48.5 LZ + ANS codec with AVX2 and NEON
SIMD acceleration. The libzuptsdk shared library is bundled under
/usr/lib/$PKGNAME -- no separate package required.
The on-disk archive extension is unchanged (.zupt); v2.x and v3.0.0
archives are bidirectionally compatible. The legacy /usr/bin/zupt
symlink is preserved for one major version cycle.
%prep
%setup -q
%build
# Pre-built before rpmbuild was invoked; nothing to do.
%install
mkdir -p %{buildroot}%{_bindir}
mkdir -p %{buildroot}%{_libdir}/$PKGNAME
mkdir -p %{buildroot}%{_docdir}/$PKGNAME
mkdir -p %{buildroot}%{_licensedir}/$PKGNAME
mkdir -p %{buildroot}%{_mandir}/man1
mkdir -p %{buildroot}%{_datadir}/bash-completion/completions
mkdir -p %{buildroot}%{_datadir}/zsh/site-functions
mkdir -p %{buildroot}%{_datadir}/fish/vendor_completions.d
install -m 755 $PKGNAME %{buildroot}%{_bindir}/$PKGNAME
ln -sf $PKGNAME %{buildroot}%{_bindir}/$LEGACY
install -m 755 libzuptsdk.so.2.0.0 %{buildroot}%{_libdir}/$PKGNAME/libzuptsdk.so.2.0.0
ln -sf libzuptsdk.so.2.0.0 %{buildroot}%{_libdir}/$PKGNAME/libzuptsdk.so.2
ln -sf libzuptsdk.so.2.0.0 %{buildroot}%{_libdir}/$PKGNAME/libzuptsdk.so
install -m 755 libpqvaptvupt.so.0.6.0 %{buildroot}%{_libdir}/$PKGNAME/libpqvaptvupt.so.0.6.0
ln -sf libpqvaptvupt.so.0.6.0 %{buildroot}%{_libdir}/$PKGNAME/libpqvaptvupt.so.0
ln -sf libpqvaptvupt.so.0.6.0 %{buildroot}%{_libdir}/$PKGNAME/libpqvaptvupt.so
install -m 644 README.md CHANGELOG.md SECURITY.md AUDIT.md %{buildroot}%{_docdir}/$PKGNAME/
install -m 644 LICENSE %{buildroot}%{_licensedir}/$PKGNAME/
if [ -f $PKGNAME.1 ]; then
install -m 644 $PKGNAME.1 %{buildroot}%{_mandir}/man1/$PKGNAME.1
gzip -9n %{buildroot}%{_mandir}/man1/$PKGNAME.1
ln -sf $PKGNAME.1.gz %{buildroot}%{_mandir}/man1/$LEGACY.1.gz
fi
if [ -f completions/vaptvupt.bash ]; then
install -m 644 completions/vaptvupt.bash %{buildroot}%{_datadir}/bash-completion/completions/$PKGNAME
ln -sf $PKGNAME %{buildroot}%{_datadir}/bash-completion/completions/$LEGACY
fi
if [ -f completions/_vaptvupt ]; then
install -m 644 completions/_vaptvupt %{buildroot}%{_datadir}/zsh/site-functions/_$PKGNAME
ln -sf _$PKGNAME %{buildroot}%{_datadir}/zsh/site-functions/_$LEGACY
fi
if [ -f completions/vaptvupt.fish ]; then
install -m 644 completions/vaptvupt.fish %{buildroot}%{_datadir}/fish/vendor_completions.d/$PKGNAME.fish
fi
%files
%license %{_licensedir}/$PKGNAME/LICENSE
%doc %{_docdir}/$PKGNAME/README.md
%doc %{_docdir}/$PKGNAME/CHANGELOG.md
%doc %{_docdir}/$PKGNAME/SECURITY.md
%doc %{_docdir}/$PKGNAME/AUDIT.md
%{_bindir}/$PKGNAME
%{_bindir}/$LEGACY
%dir %{_libdir}/$PKGNAME
%{_libdir}/$PKGNAME/libzuptsdk.so
%{_libdir}/$PKGNAME/libzuptsdk.so.2
%{_libdir}/$PKGNAME/libzuptsdk.so.2.0.0
%{_libdir}/$PKGNAME/libpqvaptvupt.so
%{_libdir}/$PKGNAME/libpqvaptvupt.so.0
%{_libdir}/$PKGNAME/libpqvaptvupt.so.0.6.0
%{_mandir}/man1/$PKGNAME.1.gz
%{_mandir}/man1/$LEGACY.1.gz
%{_datadir}/bash-completion/completions/$PKGNAME
%{_datadir}/bash-completion/completions/$LEGACY
%{_datadir}/zsh/site-functions/_$PKGNAME
%{_datadir}/zsh/site-functions/_$LEGACY
%{_datadir}/fish/vendor_completions.d/$PKGNAME.fish
%changelog
* Sun May 25 2026 Cristian Cezar Moises <zupt@riseup.net> - $VERSION-$RELEASE
- v3.0.0: Renamed from "Zupt" to "VaptVupt" because of a prior INPI
Brasil trademark on "Zupt". Archive extension .zupt is preserved;
v2.x and v3.0.0 archives are bidirectionally compatible. Legacy
/usr/bin/zupt is installed as a symlink to /usr/bin/vaptvupt.
- Integrated VaptVupt LZ + ANS codec 2.48.5: fixes csz==0 heap-
buffer-overflow READ in vv_dstream_decompress_chunk (libFuzzer-
found, medium severity), UBSan-safe pointer arithmetic in
vv_copy_match.
- Enhanced manpage (597 lines, was 422): POST-QUANTUM ENCRYPTION,
PERFORMANCE table, SECURITY/threat-model, ENVIRONMENT and
EXIT STATUS sections.
- Fixed GUI binary-discovery bug (PATH-missing-/usr/bin scenario);
GUI now does liveness check + logs discovery to stderr with
VAPTVUPT_DEBUG=1.
- 91/91 distro-safe regression suite green; F-09 byte sweep
0/1827 silent accepts; F-06 HMAC fuzz 0/2000 silent accepts.
* $changelog_date Cristian Cezar Moisés <sac@securityops.co> - $version-0
- Build the release package from audited source with optional SDK and PQBOX
features disabled.
EOF
rpmbuild --define "_topdir $top" -ba "$top/SPECS/zupt.spec"
rpmbuild --define "_topdir $RPMROOT" \
--define "_binary_payload w2.gzdio" \
-bb "$RPMROOT/SPECS/$PKGNAME.spec" 2>&1 | tail -5
mapfile -t main_rpms < <(find "$top/RPMS" -type f -name "zupt-${version}-*.rpm" \
! -name '*-debuginfo-*' ! -name '*-debugsource-*' -print | sort)
[[ ${#main_rpms[@]} -eq 1 ]] || die "expected one main RPM, found ${#main_rpms[@]}"
main_rpm=${main_rpms[0]}
RPM_PATH=$(find "$RPMROOT/RPMS" -name "$PKGNAME-${VERSION}-*.rpm" | head -1)
if [ -n "$RPM_PATH" ]; then
cp "$RPM_PATH" "/tmp/$PKGNAME-${VERSION}-${RELEASE}.${ARCH}.rpm"
echo ""
echo "Built: /tmp/$PKGNAME-${VERSION}-${RELEASE}.${ARCH}.rpm ($(du -h "/tmp/$PKGNAME-${VERSION}-${RELEASE}.${ARCH}.rpm" | cut -f1))"
rpm -qpi "/tmp/$PKGNAME-${VERSION}-${RELEASE}.${ARCH}.rpm" 2>&1 | head -15
mapfile -t source_rpms < <(find "$top/SRPMS" -type f -name "zupt-${version}-*.src.rpm" -print | sort)
[[ ${#source_rpms[@]} -eq 1 ]] || die "expected one source RPM, found ${#source_rpms[@]}"
source_rpm=${source_rpms[0]}
rpm -qpi "$main_rpm" >/dev/null
rpm -qpl "$main_rpm" > "$work/contents.txt"
if grep -Eq '(^/usr/bin/vaptvupt$|\.(o|obj|a|so|so\.[^/]+|dll|dylib)$)' "$work/contents.txt"; then
cat "$work/contents.txt" >&2
die 'forbidden alias or compiled library/object found in RPM contents'
fi
if grep -q '^/usr/local/' "$work/contents.txt"; then
cat "$work/contents.txt" >&2
die 'RPM contains files below /usr/local'
fi
(cd -- "$extract" && rpm2cpio "$main_rpm" | cpio -idm --quiet)
binary=$extract/usr/bin/zupt
[[ -x $binary ]] || die 'RPM does not contain executable /usr/bin/zupt'
if ! readelf -h "$binary" 2>/dev/null | grep -Eq 'Type:[[:space:]]+DYN'; then
die 'RPM executable is not a position-independent executable (PIE)'
fi
if ! readelf -W -l "$binary" 2>/dev/null | grep -q 'GNU_RELRO'; then
die 'RPM executable lacks a GNU_RELRO segment'
fi
stack_segment=$(readelf -W -l "$binary" 2>/dev/null | grep 'GNU_STACK' || true)
[[ -n $stack_segment && $stack_segment != *RWE* ]] || \
die 'RPM executable has a missing or executable GNU_STACK segment'
if readelf -d "$binary" 2>/dev/null | grep -Eq '(RPATH|RUNPATH)'; then
readelf -d "$binary" | grep -E '(RPATH|RUNPATH)' >&2
die 'RPM executable contains RPATH/RUNPATH'
fi
if readelf -d "$binary" 2>/dev/null | grep -Eqi '(vendor/|libvuptsdk|libpqvaptvupt)'; then
die 'RPM executable references a vendored optional library'
fi
bash scripts/test-installed-zupt.sh "$binary"
artifacts=("$main_rpm" "$source_rpm")
for artifact in "${artifacts[@]}"; do
destination=$dist_dir/$(basename -- "$artifact")
[[ ! -e $destination ]] || die "refusing to overwrite existing output: $destination"
done
for artifact in "${artifacts[@]}"; do
destination=$dist_dir/$(basename -- "$artifact")
cp -- "$artifact" "$destination"
sha256sum "$destination"
done
printf 'PASS: built and extracted-package-tested %s\n' "$dist_dir/$(basename -- "$main_rpm")"
printf 'PASS: built source RPM %s\n' "$dist_dir/$(basename -- "$source_rpm")"

View file

@ -1,3 +1,30 @@
zupt (5.2.2-1) UNRELEASED; urgency=medium
* Prepare a source-only upstream release and remove incomplete vendored SDK
and PQBOX inputs together with every precompiled-library fallback.
* Make optional integrations explicit system dependencies, disabled by
default, and preserve distribution compiler/linker flags and DESTDIR.
* Add the reusable source scanner and openSUSE/OBS source packaging.
* Restore the ZUPT/zupt application, package, executable, documentation, and
artifact names; build and test with WITH_SDK=0 WITH_PQBOX=0.
* Add explicit password prompt, file, and inherited-descriptor inputs.
* Correct the licensing record without revoking historical MIT grants present
in earlier repository revisions; current files follow current SPDX notices.
* Preserve Yann Collet's BSD-2-Clause notice for the two xxHash-derived
XXH64 source units and include it in package license metadata.
* Record the CC0-1.0 option for pq-crystals/kyber-derived ML-KEM portions
and ship the complete license text in every binary bundle.
* Preserve the BSD-3-Clause notice for curve25519-donna-derived X25519
portions and document their provenance without inventing a revision.
* Promote only license-complete release assets: Windows is ZIP-only and the
AppImage remains downstream-only pending a complete runtime source/relink
compliance handoff.
* Qualify older changelog statements about formally verified or
constant-time assembly: 5.2.2 retains source, generated output and runtime
regressions, but no reproducible formal-proof certificate for those paths.
-- Cristian Cezar Moisés <sac@securityops.co> Mon, 31 Aug 2026 00:00:00 +0000
vaptvupt (5.0.0-1) UNRELEASED; urgency=high
* ML-KEM-768 is now genuinely FIPS 203-conformant. Earlier releases shipped
@ -377,12 +404,10 @@ vaptvupt (3.0.2-1) UNRELEASED; urgency=medium
vaptvupt (3.0.1-1) UNRELEASED; urgency=medium
* GUI license cleanup: removed MIT-license credit line from the
about panel (the GUI is AGPL-3.0-or-later with commercial dual-
licensing; the MIT reference was a templating mistake). Replaced
gui/LICENSE-GUI (was MIT) with AGPL-3.0-or-later, mirroring the
top-level LICENSE. Top-level LICENSE preamble updated to reflect
the v3.0.0 Zupt → VaptVupt rename.
* GUI license metadata changed to AGPL-3.0-or-later for the then-current
source. The original entry incorrectly called earlier MIT notices a
templating mistake; the 5.2.2 erratum records that historical grants remain
valid for the exact material distributed under them.
* GUI version-string parsing bug fix: the v3.0.0 GUI used
`replace("zupt ", "")` to peel the product name out of the CLI's
version banner, but that substring also appears inside the v3.0.0

View file

@ -1,41 +1,49 @@
Source: vaptvupt
Source: zupt
Section: utils
Priority: optional
Maintainer: Cristian Cezar Moisés <sac@securityops.co>
Build-Depends:
bash,
coreutils,
debhelper-compat (= 13),
diffutils,
file,
findutils,
gcc,
gawk,
git,
grep,
gzip,
libarchive-tools,
make,
libc6-dev,
python3 (>= 3.8)
python3 (>= 3.8),
sed,
tar
Standards-Version: 4.6.2
Homepage: https://git.securityops.co/cristiancmoises/vaptvupt
Vcs-Browser: https://git.securityops.co/cristiancmoises/vaptvupt
Vcs-Git: https://git.securityops.co/cristiancmoises/vaptvupt.git
Homepage: https://github.com/cristiancmoises/zupt
Vcs-Browser: https://github.com/cristiancmoises/zupt
Vcs-Git: https://github.com/cristiancmoises/zupt.git
Rules-Requires-Root: no
Package: vaptvupt
Package: zupt
Architecture: any
Provides: zupt (= ${binary:Version})
Replaces: zupt
Conflicts: zupt
Depends: ${shlibs:Depends}, ${misc:Depends}
Description: Post-quantum backup compression utility (formerly Zupt)
VaptVupt (renamed from Zupt in v3.0.0 due to INPI Brasil trademark) is
a pure-C11 backup compression utility featuring:
Description: Post-quantum backup compression utility
ZUPT is a pure-C11 backup compression utility featuring:
* Post-quantum hybrid encryption (ML-KEM-768 + X25519, FIPS 203)
* AES-256-CTR + HMAC-SHA256 authenticated encryption (Encrypt-then-MAC)
* PBKDF2-SHA256 password key derivation (Argon2id in WITH_SDK=1 builds)
* Multi-threaded compression with the VaptVupt LZ + ANS codec 2.60.4
* Multi-threaded compression with the VaptVupt LZ + ANS codec 2.65.3
* Full-disk backup and restore with sparse-region detection
* End-to-end byte-level tamper detection on encrypted archives
(0 silent-accept positions in the v1.6 exhaustive byte sweep)
* Constant-time cryptographic primitives verified with Jasmin
* Authenticated encrypted-archive metadata and per-block integrity checks
* Portable C implementations with optional source-built assembly paths
* NIST/RFC test vectors for SHA-256, SHA-3, ML-KEM-768, AES-256-CTR,
HMAC-SHA256, X25519, PBKDF2, Argon2id
HMAC-SHA256, X25519 and PBKDF2
.
The archive extension stays .zupt for format continuity (header magic
unchanged). The binary `zupt` is preserved as a symlink to `vaptvupt`.
unchanged). The package installs only /usr/bin/zupt.
.
The archive format includes an integrity trailer that authenticates the
header and footer, per-block HMAC with bound frame-preface AAD, and
optional encrypted comments.
Encrypted archives include an integrity trailer that authenticates the
header and footer, per-block HMAC with bound frame-preface AAD, and optional
encrypted comments. Plain archives use non-cryptographic checksums.

View file

@ -1,19 +1,35 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: zupt
Upstream-Name: ZUPT
Upstream-Contact: Cristian Cezar Moisés <sac@securityops.co>
Source: https://git.securityops.co/cristiancmoises/vaptvupt
Source: https://github.com/cristiancmoises/zupt
Files: *
Copyright: 2025-2026 Cristian Cezar Moisés
License: AGPL-3.0-or-later
Files: src/vv_*.c include/vaptvupt*.h include/vv_*.h vendor/zuptsdk/include/vv_*.h vendor/zuptsdk/include/vaptvupt*.h
Files: src/vaptvupt_api.c src/vv_*.c include/vaptvupt*.h include/vv_*.h
Copyright: 2025-2026 Cristian Cezar Moisés (VaptVupt codec)
License: GPL-3.0-or-later
Files: vendor/zuptsdk/*
Copyright: 2025-2026 Cristian Cezar Moisés (libzuptsdk)
License: GPL-3.0-or-later
Files: src/zupt_xxh.c
Copyright: 2012-2021 Yann Collet
2025-2026 Cristian Cezar Moisés
License: AGPL-3.0-or-later and BSD-2-Clause
Files: src/zupt_mlkem.c
Copyright: 2025-2026 Cristian Cezar Moisés
pq-crystals/kyber contributors (adapted portions)
License: AGPL-3.0-or-later and CC0-1.0
Files: src/zupt_x25519.c
Copyright: 2008 Google Inc.
2025-2026 Cristian Cezar Moisés
License: AGPL-3.0-or-later and BSD-3-Clause
Files: src/vv_xxh64.c
Copyright: 2012-2021 Yann Collet
2025-2026 Cristian Cezar Moisés (VaptVupt codec adaptation)
License: GPL-3.0-or-later and BSD-2-Clause
Files: debian/*
Copyright: 2025-2026 Cristian Cezar Moisés <sac@securityops.co>
@ -47,3 +63,41 @@ License: GPL-3.0-or-later
.
On Debian systems, the full text of the GNU General Public License
version 3 can be found in the file `/usr/share/common-licenses/GPL-3'.
License: BSD-2-Clause
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
.
* Redistributions of source code must retain the copyright notice, this list
of conditions and the disclaimer.
* Redistributions in binary form must reproduce the copyright notice, this
list of conditions and the disclaimer in the documentation and/or other
materials provided with the distribution.
.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
License: CC0-1.0
To the extent possible under law, the upstream affirmer has waived all
copyright and related or neighboring rights to the adapted portions.
.
The complete CC0 1.0 Universal legal text is shipped in
`/usr/share/doc/zupt/LICENSE-CC0-1.0`.
License: BSD-3-Clause
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the copyright notice, conditions,
and disclaimer are retained; neither the name of Google Inc. nor contributor
names may be used to endorse derived products without prior permission.
.
The complete BSD-3-Clause notice and disclaimer are shipped in
`/usr/share/doc/zupt/LICENSE-BSD-3-Clause`.

View file

@ -2,7 +2,7 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# Honour Debian's reproducible-build epoch when set by dpkg-buildpackage.
export SOURCE_DATE_EPOCH ?= 1747699200
export SOURCE_DATE_EPOCH ?= 1788134400
# Hardening flags — Debian's defaults are already strong, this adds project-
# specific ones.
@ -15,21 +15,18 @@ export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed
override_dh_auto_build:
# Source-only build: no vendored libraries, native crypto only.
$(MAKE) WITH_SDK=0 -j$$(nproc)
$(MAKE) WITH_SDK=0 WITH_PQBOX=0 -j$$(nproc)
override_dh_auto_test:
# Distro-safe regression subset (F-06..F-12, dedup-nonce, NIST/RFC vectors).
$(MAKE) WITH_SDK=0 check
# Distro-safe quick, traversal, integrity, codec and NIST/RFC checks.
$(MAKE) WITH_SDK=0 WITH_PQBOX=0 check
override_dh_auto_install:
# Binary package is `vaptvupt` -> stage into debian/vaptvupt (dh derives the
# Binary package is `zupt` -> stage into debian/zupt (dh derives the
# staging dir from the Package: name in debian/control). Source-only: nothing
# to install beyond `make install` (no .so).
$(MAKE) DESTDIR=$(CURDIR)/debian/vaptvupt PREFIX=/usr WITH_SDK=0 install
$(MAKE) DESTDIR=$(CURDIR)/debian/zupt PREFIX=/usr \
WITH_SDK=0 WITH_PQBOX=0 INSTALL_LEGACY_ALIAS=0 install
override_dh_auto_clean:
$(MAKE) clean
# Skip dh_strip's separate -dbgsym packages for a single-source-package layout.
override_dh_strip:
dh_strip --no-automatic-dbgsym

View file

@ -0,0 +1,12 @@
README.md
CHANGELOG.md
SECURITY.md
THREAT_MODEL.md
NOTICE
THIRD-PARTY-NOTICES.md
LICENSE
LICENSE-AGPL-3.0
LICENSE-GPL-3.0
LICENSE-BSD-2-Clause
LICENSE-BSD-3-Clause
LICENSE-CC0-1.0

View file

@ -1,17 +1,17 @@
;;; SPDX-License-Identifier: AGPL-3.0-or-later
;;; Copyright (c) 2026 Cristian Cezar Moisés
;;;
;;; GNU Guix package definitions for VaptVupt (CLI + PySide6 GUI).
;;; GNU Guix package definitions for ZUPT (CLI + PySide6 GUI).
;;; Source-only build (no vendored libraries): the CLI links only libc/libm/
;;; pthread from the store.
;;;
;;; Install into your profile (additive; keeps everything else):
;;; guix package -f packaging/guix/vaptvupt.scm ; installs the GUI
;;; guix package -f packaging/guix/zupt.scm ; installs the GUI
;;; guix package -e '(@ (guix) …)' — or, for the CLI on its own:
;;; guix install -f packaging/guix/vaptvupt.scm ; (last expr = GUI)
;;; guix install -f packaging/guix/zupt.scm ; (last expr = GUI)
;;; The last expression is the GUI, which carries the CLI as an input; to get
;;; the `vaptvupt` command in your profile too, also run:
;;; guix package --install-from-expression='(begin (load "packaging/guix/vaptvupt.scm") vaptvupt)'
;;; the `zupt` command in your profile too, also run:
;;; guix package --install-from-expression='(begin (load "packaging/guix/zupt.scm") zupt)'
;;;
;;; GUI-on-Guix note: PySide6's Qt6 links several leaf libraries (libGL from
;;; mesa, libxkbcommon, the X11/xcb family, libzstd, harfbuzz, icu, ...) that are
@ -62,29 +62,30 @@
xcb-util-renderutil xcb-util-wm xcb-util-cursor
libinput-minimal mtdev libevdev eudev))
(define %vaptvupt-version "5.2.1")
(define %zupt-version "5.2.2")
(define %vaptvupt-source
(define %zupt-source
(origin
(method url-fetch)
(uri (string-append
"https://git.securityops.co/cristiancmoises/vaptvupt"
"/releases/download/v" %vaptvupt-version
"/vaptvupt-" %vaptvupt-version ".tar.gz"))
"https://github.com/cristiancmoises/zupt"
"/releases/download/v" %zupt-version
"/zupt-" %zupt-version ".tar.gz"))
(sha256
(base32 "1mzl5za5k80x74p1hb9kfi199fs74ymmlcdhhxkzxr9ls8gpg6z2"))))
(base32 "REPLACE_AFTER_FINAL_RELEASE_ARCHIVE_IS_BUILT"))))
(define-public vaptvupt
(define-public zupt
(package
(name "vaptvupt")
(version %vaptvupt-version)
(source %vaptvupt-source)
(name "zupt")
(version %zupt-version)
(source %zupt-source)
(build-system gnu-build-system)
(arguments
(list
#:make-flags
#~(list (string-append "PREFIX=" #$output)
"WITH_SDK=0"
"WITH_PQBOX=0"
(string-append "CC=" #$(cc-for-target)))
#:phases
#~(modify-phases %standard-phases
@ -94,38 +95,49 @@
;; SP 800-38A, RFC 4231/7748) are the crypto gate.
(lambda* (#:key tests? #:allow-other-keys)
(when tests?
(invoke "make" "WITH_SDK=0"
(invoke "make" "WITH_SDK=0" "WITH_PQBOX=0"
(string-append "CC=" #$(cc-for-target))
"test-vectors")
(invoke "./test_vectors")))))))
(home-page "https://git.securityops.co/cristiancmoises/vaptvupt")
(home-page "https://github.com/cristiancmoises/zupt")
(synopsis "Post-quantum backup compression utility")
(description
"VaptVupt (formerly Zupt) is a pure-C11 backup compressor with native
"ZUPT is a pure-C11 backup compressor with native
post-quantum encryption. Two in-tree PQ modes: @code{--pq} hybridizes
ML-KEM-768 (FIPS 203, validated against OpenSSL) with X25519 (recommended), and
ML-KEM-768 with X25519 (recommended), and
@code{--pq-only} uses ML-KEM-768 alone for @dfn{PQ-only} compliance postures.
Payload protection is AES-256-CTR + HMAC-SHA256 Encrypt-then-MAC with a fresh
random per-block nonce and measured constant-time tag comparison; AES-NI/SHA-NI
dispatch at runtime; the embedded VaptVupt 2.60.4 LZ+ANS codec ships
CBMC-verified BCJ filters. Password mode uses PBKDF2-SHA256. The tool is
AGPL-3.0-or-later; the embedded codec is GPL-3.0-or-later.")
(license (list license:agpl3+ license:gpl3+))))
random per-block nonce; AES-NI/SHA-NI dispatch at runtime; the bundled
VaptVupt 2.65.3 LZ+ANS codec has portable fallbacks. Password mode uses
PBKDF2-SHA256. The tool is
AGPL-3.0-or-later; the embedded codec is GPL-3.0-or-later; the two
xxHash-derived XXH64 units additionally carry BSD-2-Clause; and portions of
native ML-KEM adapted from pq-crystals/kyber carry CC0-1.0. Native X25519
portions adapted from curve25519-donna retain BSD-3-Clause.
The x86 BCJ filter and SHA-NI path also record their public-domain LZMA SDK
and SHA-Intrinsics origins; installed NOTICE and THIRD-PARTY-NOTICES.md carry
the full provenance record.")
(license (list license:agpl3+ license:gpl3+ license:bsd-2 license:bsd-3 license:cc0))))
(define-public vaptvupt-gui
(define-public zupt-gui
(package
(name "vaptvupt-gui")
(version %vaptvupt-version)
(source (package-source vaptvupt)) ; same release tarball
(name "zupt-gui")
(version %zupt-version)
(source (package-source zupt)) ; same release tarball
(build-system copy-build-system)
(arguments
(list
#:install-plan
#~'(("gui/src/zupt_gui.py" "lib/vaptvupt-gui/")
#~'(("gui/src/zupt_gui.py" "lib/zupt-gui/")
("gui/assets/zupt-icon.png"
"share/icons/hicolor/256x256/apps/vaptvupt-gui.png")
("gui/README.md" "share/doc/vaptvupt-gui/")
("gui/LICENSE-GUI" "share/doc/vaptvupt-gui/"))
"share/icons/hicolor/256x256/apps/zupt-gui.png")
("gui/README.md" "share/doc/zupt-gui/")
("LICENSE-AGPL-3.0"
"share/licenses/zupt-gui/LICENSE-AGPL-3.0")
("gui/LICENSE-GUI"
"share/licenses/zupt-gui/LICENSE-GUI")
("gui/assets/README.md"
"share/licenses/zupt-gui/ASSET-PROVENANCE.md"))
#:phases
#~(modify-phases %standard-phases
(add-after 'install 'make-launcher
@ -133,10 +145,10 @@ AGPL-3.0-or-later; the embedded codec is GPL-3.0-or-later.")
(let* ((out (assoc-ref outputs "out"))
(bin (string-append out "/bin"))
(gui (string-append
out "/lib/vaptvupt-gui/zupt_gui.py"))
out "/lib/zupt-gui/zupt_gui.py"))
(sh (search-input-file inputs "/bin/sh"))
(python3 (search-input-file inputs "/bin/python3"))
(cli (search-input-file inputs "/bin/vaptvupt"))
(cli (search-input-file inputs "/bin/zupt"))
(pyside (assoc-ref inputs "python-pyside-6"))
(site (car (find-files pyside "^site-packages$"
#:directories? #t)))
@ -156,32 +168,31 @@ AGPL-3.0-or-later; the embedded codec is GPL-3.0-or-later.")
(list (string-append zstdlib "/lib")))
":")))
(mkdir-p bin)
(call-with-output-file (string-append bin "/vaptvupt-gui")
(call-with-output-file (string-append bin "/zupt-gui")
(lambda (port)
(format port "#!~a
export VAPTVUPT_BIN=\"~a\"
export ZUPT_BIN=\"~a\"
export GUIX_PYTHONPATH=\"~a:~a${GUIX_PYTHONPATH:+:}$GUIX_PYTHONPATH\"
export QT_PLUGIN_PATH=\"~a/lib/qt6/plugins:~a/lib/qt6/plugins${QT_PLUGIN_PATH:+:}$QT_PLUGIN_PATH\"
export LD_LIBRARY_PATH=\"~a${LD_LIBRARY_PATH:+:}$LD_LIBRARY_PATH\"
exec \"~a\" \"~a\" \"$@\"\n"
sh cli site shsite qtbase qtwl ldpath python3 gui)))
(chmod (string-append bin "/vaptvupt-gui") #o755)
(symlink "vaptvupt-gui" (string-append bin "/zupt-gui")))))
(chmod (string-append bin "/zupt-gui") #o755))))
(add-after 'make-launcher 'install-desktop-file
(lambda* (#:key outputs #:allow-other-keys)
(let* ((out (assoc-ref outputs "out"))
(apps (string-append out "/share/applications")))
(mkdir-p apps)
(call-with-output-file
(string-append apps "/vaptvupt-gui.desktop")
(string-append apps "/zupt-gui.desktop")
(lambda (port)
(format port "[Desktop Entry]
Type=Application
Name=VaptVupt
Name=ZUPT
GenericName=Post-Quantum Backup
Comment=Compress, encrypt and restore .zupt archives
Exec=~a/bin/vaptvupt-gui %F
Icon=vaptvupt-gui
Exec=~a/bin/zupt-gui %F
Icon=zupt-gui
Terminal=false
Categories=Utility;Archiving;Security;
MimeType=application/x-zupt;
@ -189,20 +200,20 @@ Keywords=backup;encryption;post-quantum;compression;zupt;\n"
out)))))))))
(inputs
(append (list bash-minimal python python-pyside-6 python-shiboken-6
qtbase qtwayland vaptvupt
qtbase qtwayland zupt
(list zstd "lib")) ; libzstd.so.1 is in zstd's "lib" output
%gui-runtime-libs))
(home-page "https://git.securityops.co/cristiancmoises/vaptvupt")
(synopsis "Desktop frontend for the VaptVupt post-quantum backup tool")
(home-page "https://github.com/cristiancmoises/zupt")
(synopsis "Desktop frontend for the ZUPT post-quantum backup tool")
(description
"PySide6 (Qt 6) graphical frontend for VaptVupt: create, inspect and
"PySide6 (Qt 6) graphical frontend for ZUPT: create, inspect and
extract @code{.zupt} archives with password or post-quantum recipient
encryption, including the @code{--pq} hybrid and @code{--pq-only} full
post-quantum modes. The launcher pins the matching @code{vaptvupt} CLI from the
store via @env{VAPTVUPT_BIN} and sets @env{LD_LIBRARY_PATH} to the Qt6 leaf
post-quantum modes. The launcher pins the matching @code{zupt} CLI from the
store via @env{ZUPT_BIN} and sets @env{LD_LIBRARY_PATH} to the Qt6 leaf
libraries PySide6 needs but does not carry in its RUNPATH.")
(license license:agpl3+)))
;; `guix package -f' evaluates the file's last expression — the GUI, which
;; carries the CLI as an input.
vaptvupt-gui
zupt-gui

View file

@ -1,9 +1,9 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# Homebrew formula for zupt.
# Homebrew formula for ZUPT.
#
# To publish:
# 1. Run `make dist` upstream to produce zupt-VERSION.tar.gz (reproducible).
# 1. Run `make dist` upstream to produce zupt-VERSION.tar.gz.
# 2. Upload to a stable release URL.
# 3. Update `url`, `version`, and `sha256` below.
# 4. Submit to homebrew-core via PR OR host in your own tap
@ -19,13 +19,13 @@
# the C fallback for AES-256-CTR / HMAC compare paths is shipped.
# * Source-only build: no vendored libraries; native crypto only.
class Vaptvupt < Formula
class Zupt < Formula
desc "Post-quantum backup compression utility (ML-KEM-768 + AES-256-CTR + HMAC-SHA256)"
homepage "https://git.securityops.co/cristiancmoises/vaptvupt"
url "https://git.securityops.co/cristiancmoises/vaptvupt/releases/download/v5.2.1/vaptvupt-5.2.1.tar.gz"
version "5.2.1"
homepage "https://github.com/cristiancmoises/zupt"
url "https://github.com/cristiancmoises/zupt/releases/download/v5.2.2/zupt-5.2.2.tar.gz"
version "5.2.2"
sha256 "REPLACE_WITH_SHA256_OF_RELEASE_TARBALL"
license "AGPL-3.0-or-later"
license all_of: ["AGPL-3.0-or-later", "GPL-3.0-or-later", "BSD-2-Clause", "BSD-3-Clause", "CC0-1.0"]
depends_on "python@3.12" => :test # only for test-suite tamper harness
@ -35,18 +35,24 @@ class Vaptvupt < Formula
# this and falls back cleanly.
ENV["CFLAGS"] = "#{ENV.cflags} -O2 -std=c11 -Wall -Wextra"
system "make", "WITH_SDK=0", "-j#{ENV.make_jobs}"
system "make", "DESTDIR=#{prefix}", "PREFIX=", "WITH_SDK=0", "install"
system "make", "WITH_SDK=0", "WITH_PQBOX=0", "-j#{ENV.make_jobs}"
system "make", "PREFIX=#{prefix}", "WITH_SDK=0", "WITH_PQBOX=0",
"INSTALL_LEGACY_ALIAS=0", "install"
# Docs (no vendored .so/.dylib in the source-only build).
# Docs (no vendored .so/.dylib in the source-only build). `make install`
# also installs the complete project license/notice set.
doc.install "README.md", "SECURITY.md", "CHANGELOG.md"
%w[LICENSE-BSD-3-Clause LICENSE-CC0-1.0].each do |notice|
odie "missing installed license #{notice}" unless \
(share/"licenses/zupt"/notice).exist?
end
end
test do
# End-to-end sanity check: build a real archive, extract it, byte-compare.
(testpath/"input.txt").write("homebrew formula test payload\n")
system bin/"zupt", "c", "-p", "test", "out.zupt", "input.txt"
system bin/"zupt", "info", "out.zupt"
system bin/"zupt", "t", "-p", "test", "out.zupt"
mkdir "extracted"
cd "extracted" do
system bin/"zupt", "x", "-p", "test", "../out.zupt"

View file

@ -1,139 +1,8 @@
#!/bin/bash
#!/usr/bin/env 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.3_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.3"
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.3-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.3-linux-x86_64.tar.gz"
if [ -f "$ZUPT_CLI_TAR" ]; then
tar -xzf "$ZUPT_CLI_TAR" -C /opt/
ln -sf /opt/zupt-2.2.3-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.3"
# Stable entry point for the source installer. Dependency installation belongs
# to the operating-system package manager; this script performs no downloads.
set -Eeuo pipefail
repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)
exec "$repo_root/gui/install.sh" "$@"

View file

@ -1,25 +1,22 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# Nix flake for zupt.
# Nix flake for ZUPT.
#
# Usage (with flakes enabled):
# nix build .#zupt # build the package
# nix run .#zupt -- version # run zupt directly
# nix run .#zupt -- --version # run ZUPT directly
# nix develop # drop into a dev shell
# nix flake check # lint the flake
#
# To consume from another flake:
# inputs.zupt.url = "git+https://git.securityops.co/cristiancmoises/zupt?ref=v2.4.4";
# inputs.zupt.url = "github:cristiancmoises/zupt/v5.2.2";
# ...packages.x86_64-linux.default = inputs.zupt.packages.x86_64-linux.zupt;
#
# Reproducibility:
# * Nix already pins the source tree by hash.
# * `make dist` is also reproducible (tests/test_dist_reproducible.sh).
# * Together, two independent Nix evaluations of the same flake.lock
# produce byte-identical /nix/store outputs.
# `make dist` has its own reproducibility gate. This development flake has no
# committed lock file and therefore makes no independent locked-output claim.
{
description = "Zupt post-quantum backup compression utility (C11)";
description = "ZUPT post-quantum backup compression utility (C11)";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
@ -27,22 +24,25 @@
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachSystem [ "x86_64-linux" "aarch64-linux" ] (system:
flake-utils.lib.eachSystem [ "x86_64-linux" ] (system:
let
pkgs = import nixpkgs { inherit system; };
zupt = pkgs.stdenv.mkDerivation {
pname = "vaptvupt";
version = "5.0.0";
pname = "zupt";
version = "5.2.2";
# When publishing, replace this with `fetchurl` against the
# release tarball. For local development the flake assumes it
# lives in the same directory as the source.
src = ./.;
src = builtins.path { path = ../..; name = "zupt-source"; };
nativeBuildInputs = with pkgs; [
gcc
git
gnumake
file
gnutar
];
# python3 is only used by the regression-test harness.
@ -55,7 +55,7 @@
# Source-only build (WITH_SDK=0): native crypto, no vendored libraries.
buildPhase = ''
runHook preBuild
make WITH_SDK=0 -j$NIX_BUILD_CORES
make WITH_SDK=0 WITH_PQBOX=0 -j$NIX_BUILD_CORES
runHook postBuild
'';
@ -63,27 +63,30 @@
doCheck = true;
checkPhase = ''
runHook preCheck
make WITH_SDK=0 check
make WITH_SDK=0 WITH_PQBOX=0 check
runHook postCheck
'';
installPhase = ''
runHook preInstall
make DESTDIR=$out PREFIX= WITH_SDK=0 install
make PREFIX=$out WITH_SDK=0 WITH_PQBOX=0 \
INSTALL_LEGACY_ALIAS=0 install
# Docs
mkdir -p $out/share/doc/vaptvupt
cp README.md SECURITY.md CHANGELOG.md $out/share/doc/vaptvupt/
mkdir -p $out/share/doc/zupt
cp README.md SECURITY.md CHANGELOG.md $out/share/doc/zupt/
test -f $out/share/licenses/zupt/LICENSE-BSD-3-Clause
test -f $out/share/licenses/zupt/LICENSE-CC0-1.0
runHook postInstall
'';
meta = with pkgs.lib; {
description = "Post-quantum backup compression utility (ML-KEM-768 + X25519 + AES-256-CTR + HMAC-SHA256)";
homepage = "https://git.securityops.co/cristiancmoises/vaptvupt";
license = with licenses; [ agpl3Plus gpl3Plus ];
homepage = "https://github.com/cristiancmoises/zupt";
license = with licenses; [ agpl3Plus gpl3Plus bsd2 bsd3 cc0 ];
maintainers = [ ];
platforms = [ "x86_64-linux" "aarch64-linux" ];
mainProgram = "vaptvupt";
platforms = [ "x86_64-linux" ];
mainProgram = "zupt";
};
};
in {

View file

@ -1,88 +1,220 @@
# openSUSE Build Service update for `home:cabelo:innovators/vaptvupt`
# ZUPT 5.2.2 for openSUSE Build Service
This directory contains the three files needed to build vaptvupt `5.0.0`
in OBS:
This directory is the upstream, source-only OBS recipe for ZUPT. It is a
handoff for the downstream maintainer; its presence does not mean that the
package has been submitted to or accepted by openSUSE Factory.
| File | Purpose |
|---------------|-------------------------------------------------------------------------|
| `_service` | `revision` pinned to `v5.0.0`. Format unchanged (still `tar_scm`). |
| `vaptvupt.spec` | `Version: 5.0.0`. `License: AGPL-3.0-or-later`. `%check` calls `make check`. |
| `vaptvupt.changes`| Changelog for the 4.x series. Older history preserved verbatim. |
Cristian Cezar Moisés, ZUPT's creator and current upstream maintainer,
prepared the 5.2.2 source, build, test, documentation, and upstream packaging
changes in this handoff. Alessandro de Oliveira Faria (Cabelo) is credited only
as the openSUSE collaborator and downstream OBS package maintainer: he reviews
the handoff, commits it through the portal/project he maintains, and may make
the openSUSE-side adjustments he considers necessary. This role does not
attribute upstream code or the 5.2.2 upstream changes to Cabelo.
## Spec notes
## Files and source policy
1. **License**`AGPL-3.0-or-later` (dual-licensed AGPL-3.0-or-later
+ commercial).
| File | Purpose |
|---|---|
| `_service` | Fetch the immutable `v5.2.2` tag and create `Source0` at build time. |
| `zupt.spec` | Build and test the CLI with optional external system integrations disabled. |
| `zupt.changes` | openSUSE-format package history. |
| `source-audit.sh` | Handoff wrapper for the repository scanner; run it from the complete handoff tree. |
2. **No BuildRequires beyond the toolchain** — the default build needs
only `gcc gzip make` (plus `libm`/`pthread` from glibc). There are
**no system library BuildRequires**. The repository is source-only:
the previously vendored `libzuptsdk.so` and `libpqvaptvupt.so` have
been removed from the tree, `%build` and `%install` run with
`WITH_SDK=0`, and `%files` no longer lists any `.so`. The package
installs no shared library. Do not add system crypto BuildRequires.
The source service uses `obs_scm`, with Git submodules and Git LFS explicitly
disabled. Its primary URL is the canonical upstream:
The optional SDK modes (`--pq-sdk`, `--pq-box`) and the Argon2id KDF
require an upstream `make WITH_SDK=1` build linked against the
separately distributed `libzuptsdk`/`libpqvaptvupt` libraries. They
are not part of this package.
3. **`%check` target** — the s390x branch falls back to `test-vectors`;
other architectures run `make check`. This exercises the HMAC tamper
detection, archive-integrity trailer, byte-level integrity preface
AAD, default-KDF, auth-fail, and encrypted-comment suites, the
NIST/RFC vectors (SHA-256, SHA-3, ML-KEM-768, AES-256-CTR, HMAC,
X25519, PBKDF2), and the path-traversal, argument-order, and
block-swap regressions.
The default password KDF is **PBKDF2-SHA256** (600k iterations).
Argon2id test vectors run only in a `WITH_SDK=1` build and are not
checked here.
4. **URLs** — the `URL:` field points at the canonical project URL
`https://git.securityops.co/cristiancmoises/vaptvupt`. The `_service`
file still fetches from GitHub
(`https://github.com/cristiancmoises/vaptvupt`), which is what the
existing `tar_scm` configuration uses in OBS.
## How to apply
```sh
# 1. Check out the package
osc checkout home:cabelo:innovators vaptvupt
cd home:cabelo:innovators/vaptvupt
# 2. Drop the new files in (assuming this README is at
# /path/to/vaptvupt-source/packaging/opensuse/README.md)
cp /path/to/vaptvupt-source/packaging/opensuse/_service .
cp /path/to/vaptvupt-source/packaging/opensuse/vaptvupt.spec .
cp /path/to/vaptvupt-source/packaging/opensuse/vaptvupt.changes .
# 3. Trigger the service locally to fetch v5.0.0 from GitHub
osc service runall
# Produces vaptvupt-5.0.0.tar.gz in the current directory.
# 4. (Optional) Local build to verify before committing
osc build openSUSE_Tumbleweed x86_64
# 5. Commit upstream
osc status # confirm vaptvupt-5.0.0.tar.gz is staged alongside the
# three text files
osc commit -m "Update to 5.0.0"
```text
https://github.com/cristiancmoises/zupt.git
```
## Notes for future updates
`obs_scm` stores an `.obscpio` plus `.obsinfo`. The `tar` and `recompress`
services reconstruct `zupt-5.2.2.tar.gz` inside the build environment, which
matches `Source0` in the spec.
* The `_service` `revision` is pinned to `v5.0.0`. To track a new
release, edit that one line and re-run `osc service runall`.
* The spec's `Version:` field is hard-coded — when you bump `_service`
`revision`, also bump `Version:` to match.
* `BuildRequires` is intentionally minimal (`gcc gzip make`). vaptvupt
has no external library dependencies in the default build; do not add
system crypto BuildRequires.
This source policy does not prohibit separately built release-page packages.
The upstream 5.2.2 gates may publish the CLI source tarball, DEB, binary RPM,
SRPM, notice-bearing Linux tar.xz, Windows ZIP, and macOS DMG, together with a
GUI DEB, noarch RPM, GUI SRPM, and source-only portable GUI ZIP after each
format-specific test succeeds. None of those files is an OBS `Source0` input
or belongs in Git. AppImage and bare executables remain excluded: the former
lacks an audited runtime source/relink handoff, while the latter does not carry
the required license and notice payload beside the program.
## Reporting issues
## License and bundled codec
* Upstream bugs: https://git.securityops.co/cristiancmoises/vaptvupt
* openSUSE packaging bugs: https://bugs.opensuse.org/
* Cabelo's OBS project: https://build.opensuse.org/project/show/home:cabelo:innovators
The resulting executable combines the AGPL-3.0-or-later application with the
GPL-3.0-or-later VaptVupt codec, adapted BSD-2-Clause XXH64 routines, and
CC0-1.0 pq-crystals/kyber-derived ML-KEM portions, plus BSD-3-Clause
curve25519-donna-derived X25519 portions, so the RPM uses:
```text
AGPL-3.0-or-later AND GPL-3.0-or-later AND BSD-2-Clause AND BSD-3-Clause AND CC0-1.0
```
The bundled codec is VaptVupt codec tag `v2.65.3`. It was integrated into this
repository by commit `59f9ebc59ea13c6edf1d199ca795cdbf00e62226` and is declared
as `bundled(vaptvupt-codec) = 2.65.3`. That integration commit records the local
ANS safe-zone reserve patch applied on top of the upstream tag. The package
retains all license and notice files, including Yann Collet's xxHash notice;
it does not claim that the codec is unbundled.
## Optional SDK and PQBOX integrations
The OBS package always builds with:
```text
WITH_SDK=0 WITH_PQBOX=0
```
The resulting CLI retains the in-tree password, ML-KEM-768, X25519 and hybrid
features. It does not enable the optional libvuptsdk-backed Argon2id/`--pq-sdk`
integration or the separate libpqvaptvupt-backed `--pq-box` integration. Those
options may only be enabled in a future package after their complete source or
system development packages, licenses, ABI and dependencies have been audited.
The build does not download dependencies and never loads a repository-local
`.so`, `.a` or `.o` fallback.
## Archive integrity and compatibility in 5.2.2
New encrypted archives bind every DATA and DEDUP_REF frame to its logical
position. An authenticated reference also carries the authenticated position of
the source DATA frame, and new disk archives use flag-gated index/content-hash
metadata. The on-disk version byte remains 1.6, but an older reader is not
claimed to accept every new 5.2.2 encoding.
The packaged `extract`, `list`, `test`, and `disk restore` paths require an
archive-integrity trailer by default, without trusting unauthenticated header
flags. `--allow-legacy-no-ait` is accepted only by those commands for recovery
of a known, trusted pre-AIT archive and emits a downgrade warning. `info` merely
reports unauthenticated framing and apparent AIT presence; it does not validate
the trailer or contents. Package documentation must not recommend the override
for untrusted input or present `info` success as an integrity result.
The separate v5.2.1 compatibility claim is narrow: an actual
password-encrypted, deduplicated DATA/DATA/REF/DATA disk archive created from the
immutable v5.2.1 tag is stored as hexadecimal text with its source and SHA-256
provenance. The 5.2.2 reader reconstructs the legacy linear block-AAD sequence,
lists, tests, extracts, and restores its input byte-exact through the
fixed-width legacy disk-index parser. This does not cover every historical mode
and must be rerun on the final candidate before it is promoted as a release
gate.
Disk restore also snapshots the measured archive into a private scratch file
before opening the destination, then validates and restores from that same
stream. An invalid `ZUPT_TMPDIR` override (or the compatibility fallback
`VAPTVUPT_TMPDIR`) and an unknown or insufficient raw-device capacity fail
before the first target write. The package check covers
the unprivileged unknown-capacity path; its loop-device size regression is
reported `SKIP`, not `PASS`, when the builder cannot create a loop device.
## Migration from the former package name
The main package is named `zupt` and installs only `/usr/bin/zupt`, its man
page, and its completions. The spec has a versioned `Provides: vaptvupt` and
`Obsoletes: vaptvupt` so an installed package under the former public name can
upgrade cleanly. It intentionally does not claim or install a second
`/usr/bin/vaptvupt` executable. The bundled codec and optional library keep
their established VaptVupt identifiers because those are compatibility-facing
API names, not the application package name.
## Local validation workflow
Run these commands in an OBS package checkout, not in the upstream Git tree:
```sh
xmllint --noout _service
osc service manualrun
rpmspec -P zupt.spec >/dev/null
spec-cleaner --diff zupt.spec
osc build --clean --keep-pkgs="$PWD/.osc-build-results" \
openSUSE_Tumbleweed x86_64
rpmlint .osc-build-results/*.rpm
```
`osc service manualrun` materializes the service marked `manual` (the pinned
SCM input). The tarball itself is
reconstructed by the build-time services. Neither `%build` nor `%check` may
access the network.
For a source RPM check outside OBS, place the service-produced
`zupt-5.2.2.tar.gz` next to the spec and use a disposable RPM build tree:
```sh
rpm_top=$(mktemp -d)
trap 'rm -rf -- "$rpm_top"' EXIT
mkdir -p "$rpm_top"/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
rpmbuild --define "_topdir $rpm_top" --define "_sourcedir $PWD" \
-bs zupt.spec
```
After building, inspect the RPM contents and dependencies, run `rpmlint`, then
install it in a disposable openSUSE environment and execute
`scripts/test-installed-zupt.sh`. Do not claim a repository or architecture
as supported until its build and installed smoke test have actually passed.
## Validation matrix for this handoff
The local results below were produced on 2026-08-24 from the 5.2.2 candidate
snapshot captured for the packaging run, in a disposable openSUSE Tumbleweed
20260822 x86_64 container. This matrix was documented afterward, so the results
validate that captured snapshot, not the later documentation edit, a future
commit or a tag. Commit- and tag-dependent checks must be repeated after the
final commit; the validation tarball checksum below is not a release checksum.
`SKIP` is not success.
| Gate | Result | Evidence |
|---|---|---|
| `_service` XML syntax | PASS | `xmllint --noout`; installed service definitions and parameters also exercised locally. |
| ShellCheck for packaging, export, source-policy, and security regression scripts | PASS | ShellCheck 0.10.0 returned zero for the scripts listed in the CI source-policy job, including the scanner and new archive/disk regressions; repeat after the final commit/tag. |
| Upstream source-only scanner and adversarial scanner tests | PASS | Clean snapshot: 191 files; OBS tar: 191 files/1 archive; SRPM tree: 193 files/1 archive; 29 positive/negative scanner regressions passed. |
| Reproducible source archive (two builds, same SHA-256) | PASS | Two local `obs_scm`/`tar`/`recompress` runs were byte-identical (`39e59f5e...`, validation only; regenerate after the real tag). |
| Upstream build, `make check`, and `make test-all` | SKIP | The real RPM `%check`/`make check` passed; an exact-candidate `make test-all` result was not produced by this packaging run. |
| Positional DATA/DEDUP_REF AAD and mandatory-AIT regressions | PASS | `%check` passed AIT removal, F-09 preface, DATA/REF reorder/replay, little-endian, varint and atomic-output regressions. |
| v5.2.1 encrypted+dedup disk compatibility | PASS | Working-tree candidate decoded the textual 718-byte v5.2.1 DATA/DATA/REF/DATA fixture, then `list`, `test`, generic extraction, and byte-exact disk restore passed; repeat after the final commit/tag. |
| `rpmspec` parse | PASS | Both `rpmspec -P` and `rpmspec --parse` returned zero; Source0 resolved to `zupt-5.2.2.tar.gz`. |
| `spec-cleaner` | PASS | Version 1.2.4+2 returned zero and proposed no diff. |
| `rpmbuild` source and binary RPM | PASS | `rpmbuild -bs` and `-ba` passed from the service-generated Source0 with the openSUSE `.changes` conversion. |
| `rpmlint` main RPM + SRPM | PASS | 0 errors and one `invalid-url Source0` warning for the service-generated local Source0; no `rpmlintrc` or suppression was added. |
| `rpmlint` including automatic debug packages | FAIL | `debugsource: no-binary` error and expected `debuginfo: unstripped-binary-or-object` warning from the complete generated package set; debug packages were not disabled or suppressed. |
| `osc service` | PASS | Installed `obs_scm` 0.12.4, `tar` 0.12.4 and `recompress` 0.5.2 produced the correctly named source tar locally; canonical tag fetch remains tag-dependent. |
| Tumbleweed x86_64 local build/install/round trip/uninstall | PASS | Tumbleweed 20260822 container: RPM `%check`, root and `nobody` installed tests, content/hardening audit and clean uninstall passed. This is not an OBS/Factory result. |
| Official OBS `osc build` invocation | FAIL | The command reached `https://api.opensuse.org` but returned HTTP 401 because no OBS credentials are configured. |
| Factory/Tumbleweed x86_64 OBS validation | SKIP | The failed authenticated `osc build` invocation produced no Factory build result; local Tumbleweed evidence is not promoted to Factory evidence. |
| aarch64, ppc64le, s390x, riscv64 | SKIP | No build evidence yet. |
| Leap and SLE | SKIP | No build evidence yet. |
`SKIP` is not success. Factory/Tumbleweed x86_64 remains the primary downstream
gate.
## Handoff procedure for Alessandro/Cabelo
1. Upstream creates and verifies the annotated `v5.2.2` tag only after all
mandatory gates pass.
2. With Git, `file`, bsdtar, tar, zip, unzip and SHA-256 tools installed, run
`scripts/export-opensuse-package.sh v5.2.2`. Verify the reported ZIP and
SHA-256 outside the Git index. The handoff includes both
`packaging/opensuse/source-audit.sh` and its required
`scripts/check-source-only.sh`; keep that relative layout while auditing.
3. Check out the OBS package:
```sh
osc checkout home:cabelo:innovators zupt
cd home:cabelo:innovators/zupt
```
4. From the extracted handoff root, run
`packaging/opensuse/source-audit.sh --archive /path/to/zupt-5.2.2.tar.gz`.
Then copy `_service`, `zupt.spec`, `zupt.changes` and `README.md`
into the flat OBS package checkout. The audit wrapper is not an OBS build
source and must not be copied without its companion `scripts/` directory.
5. Run the local validation workflow above, including the installed round-trip
test. Build every repository and architecture enabled in the OBS project;
record failures or unavailable gates as such.
6. Review `osc diff`, confirm that no RPM or other binary was added as a source,
and commit to OBS only after the required gates pass.
For future releases, increment the stable patch version, create a new immutable
tag, update the matching revision/version in `_service`, spec and changes, run
the source-only scanner, regenerate the handoff, and repeat every OBS gate.
Never move an existing tag or consume forge release binaries as `Source0`.

View file

@ -1,16 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
<services>
<service name="tar_scm" mode="manual">
<param name="url">https://github.com/cristiancmoises/vaptvupt</param>
<param name="scm">git</param>
<param name="revision">v5.0.0</param>
<param name="versionformat">@PARENT_TAG@</param>
<param name="versionrewrite-pattern">v(.*)</param>
<param name="submodules">enable</param>
<param name="filename">vaptvupt</param>
</service>
<service name="recompress" mode="manual">
<param name="file">*.tar</param>
<param name="compression">gz</param>
</service>
<service name="set_version" mode="manual"/>
<service name="obs_scm" mode="manual">
<param name="url">https://github.com/cristiancmoises/zupt.git</param>
<param name="scm">git</param>
<param name="revision">refs/tags/v5.2.2</param>
<param name="versionformat">@PARENT_TAG@</param>
<param name="versionrewrite-pattern">^v(.*)$</param>
<param name="versionrewrite-replacement">\1</param>
<param name="filename">zupt</param>
<param name="submodules">disable</param>
<param name="lfs">disable</param>
</service>
<service name="tar" mode="buildtime"/>
<service name="recompress" mode="buildtime">
<param name="file">*.tar</param>
<param name="compression">gz</param>
</service>
</services>

View file

@ -0,0 +1,14 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: AGPL-3.0-or-later
set -Eeuo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)
SCANNER=$SCRIPT_DIR/../../scripts/check-source-only.sh
if [[ ! -x $SCANNER ]]; then
printf 'ERROR: source-only scanner is missing or not executable: %s\n' "$SCANNER" >&2
exit 2
fi
exec "$SCANNER" "$@"

View file

@ -1,113 +0,0 @@
#
# spec file for package vaptvupt
#
# Copyright (c) 2026 SUSE LLC
# Copyright (c) 2026 Alessandro de Oliveira Faria (A.K.A CABELO) <cabelo@opensuse.org>
# Copyright (c) 2025-2026 Cristian Cezar Moisés <zupt@riseup.net> (upstream)
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
# upon. The license for this file, and modifications and additions to the
# file, is the same license as for the pristine package itself (unless the
# license for the pristine package is not an Open Source License, in which
# case the license is the MIT License). An "Open Source License" is a
# license that conforms to the Open Source Definition (Version 1.9)
# published by the Open Source Initiative.
# Please submit bugfixes or comments via https://bugs.opensuse.org/
#
Name: vaptvupt
Version: 5.2.1
Release: 0
Summary: Post-quantum backup compression with AES-256 + ML-KEM-768 hybrid encryption
License: AGPL-3.0-or-later
Group: Productivity/Archiving/Compression
URL: https://git.securityops.co/cristiancmoises/vaptvupt
Source0: %{name}-%{version}.tar.gz
BuildRequires: gcc
BuildRequires: gzip
BuildRequires: make
# v3.0.0 renamed the project Zupt -> VaptVupt (prior INPI Brasil
# trademark on "Zupt"). Cleanly supersede any installed zupt package;
# the binary still installs a /usr/bin/zupt compatibility symlink.
Provides: zupt = %{version}-%{release}
Obsoletes: zupt < 3.0.0
%description
VaptVupt (formerly Zupt; renamed in v3.0.0 due to a prior INPI Brasil
trademark on the name "Zupt") compresses and encrypts backup archives.
LZ + ANS compression (VaptVupt codec, ~2-3 GB/s decompression on x86_64
with AVX2 / aarch64 with NEON), AES-256-CTR + HMAC-SHA256 per-block
authenticated encryption, multi-threaded, with ML-KEM-768 + X25519
post-quantum hybrid key encapsulation (FIPS 203 + RFC 7748) via --pq.
This package builds entirely from source with no external library
dependency. The password KDF is PBKDF2-SHA256 (600k iterations). The
optional libzuptsdk-backed modes (Argon2id KDF, --pq-sdk, --pq-box) are
not built here; they require an upstream WITH_SDK=1 build against the
separately distributed libzuptsdk/libpqvaptvupt.
Pure C11, ~5,000 lines of core code. Constant-time cryptographic
primitives are formally verified with Jasmin on x86_64
(zupt_mac_verify_ct, zupt_ct_select_32); a clean C fallback runs on
aarch64 and other architectures.
%prep
%autosetup -p1
chmod +x tests/*.sh
%build
%make_build V=1 WITH_SDK=0 \
CFLAGS="%{optflags} -fPIE -Wall -Wextra -std=c11 -Iinclude -Isrc" \
LDFLAGS="%{?build_ldflags} -pie" \
LDLIBS="-lm -lpthread"
%check
# `make check` is the distro-safe subset added in 2.4.8: runs the
# security-critical regressions (F-06 HMAC, F-08 AIT, F-09 byte
# integrity, F-10 KDF, F-11 auth-fail, F-12 comments) plus NIST/RFC
# vectors. Skips threaded and dist-reproducibility tests that are
# sensitive to build-host environment.
#
# On s390x, fall back to just the vector tests (Jasmin assembly is
# x86_64-only; threading harness has been flaky on big-endian).
%ifarch s390x
%make_build V=1 WITH_SDK=0 \
CFLAGS="%{optflags} -fPIE -Wall -Wextra -std=c11 -Iinclude -Isrc" \
LDFLAGS="%{?build_ldflags} -pie" \
LDLIBS="-lm -lpthread" \
test-vectors
./test_vectors
%else
%make_build V=1 WITH_SDK=0 \
CFLAGS="%{optflags} -fPIE -Wall -Wextra -std=c11 -Iinclude -Isrc" \
LDFLAGS="%{?build_ldflags} -pie" \
LDLIBS="-lm -lpthread" \
check
%endif
%install
%make_install WITH_SDK=0 PREFIX=%{_prefix}
%files
%license LICENSE
%doc README.md SECURITY.md CHANGELOG.md
%{_bindir}/vaptvupt
%{_bindir}/zupt
%{_datadir}/bash-completion/completions/vaptvupt
%{_datadir}/bash-completion/completions/zupt
%{_datadir}/zsh/site-functions/_vaptvupt
%{_datadir}/zsh/site-functions/_zupt
%{_datadir}/fish/vendor_completions.d/vaptvupt.fish
%{_mandir}/man1/vaptvupt.1%{?ext_man}
%{_mandir}/man1/zupt.1%{?ext_man}
%changelog
* Sat Jul 11 2026 Cristian Cezar Moisés <sac@securityops.co> - 5.1.0-1
- Codec 2.65.0; large compression-ratio gains (auto format_v2 + level-scaled
block window); --dedup keeps a small block; GUI compress-hang and
job-completion-crash fixes. Wire format unchanged (v1.6).

View file

@ -1,3 +1,69 @@
-------------------------------------------------------------------
Mon Aug 31 00:00:00 UTC 2026 - Cristian Cezar Moisés <sac@securityops.co>
- Update to 5.2.2:
* Convert the upstream and OBS inputs to a source-only release: remove
precompiled library and object inputs and reject their reintroduction with
reusable source-archive auditing.
* Build with WITH_SDK=0 and WITH_PQBOX=0. The optional integrations now
require an explicit source or packaged system dependency and never use a
private precompiled fallback.
* Preserve portable compiler and linker flags, architecture-local optimized
translation units, scalar fallbacks, parallel build, and staged DESTDIR
installation.
* Build the packaged executable as PIE with full RELRO/NOW and a
non-executable stack while preserving automatic debuginfo generation and
avoiding manual stripping or RPATH/RUNPATH.
* Update OBS source services to obs_scm pinned to the immutable v5.2.2 tag;
disable submodules and Git LFS and create the compressed tarball at build
time.
* Run the real upstream check target from the RPM check phase without
architecture-specific test suppression.
* Harden archive extraction against traversal, symlink/hardlink and Windows
reparse-point races; publish only fully size/checksum-verified temporary
output and add structurally valid hostile-archive regression fixtures.
* Reject normal, solid, and disk-backup output aliases of an input file,
including alternate spellings, hardlinks, and symlinks, before creating the
output; --force cannot bypass this data-loss guard.
* Snapshot disk-restore input privately before opening its destructive
destination and restore from the same validated stream. Reject raw devices
whose capacity is unknown or smaller than the image before the first write.
* Enforce DATA frame types across serial, threaded, solid, test, and disk
readers, and retain the exact encrypted+dedup AAD sequence used by v5.2.1.
Test an actual v5.2.1 password-encrypted DATA/DATA/REF/DATA disk fixture
through list, test, generic extraction, and disk restore.
* Use random private benchmark scratch directories and remove them without
following links instead of using a predictable process-ID path.
* Package the AGPL-3.0-or-later application together with the bundled
GPL-3.0-or-later VaptVupt codec 2.65.3 and the BSD-2-Clause XXH64-derived
routines; preserve all applicable notices.
* Rename the application and package back to ZUPT/zupt. Install only the
zupt command and add versioned Provides/Obsoletes for migration from the
former vaptvupt package without shipping a duplicate executable.
* Add the source-only openSUSE handoff/export workflow and validation matrix.
* Add explicit password prompt, file, and inherited-descriptor inputs.
* Validate the source audit, rpmbuild -bs/-ba, the complete RPM check phase,
package contents and dependencies, installed round trips, and clean
uninstall in a disposable openSUSE Tumbleweed 20260822 x86_64 container.
OBS/Factory, other architectures, Leap, and SLE remain separate unexecuted
downstream gates and are not claimed by this validation.
* Correct the licensing record without revoking historical MIT grants present
in earlier repository revisions; current files follow current SPDX notices.
* Correct the stale public-domain statement for XXH64-derived code and retain
Yann Collet's BSD-2-Clause copyright, conditions, and disclaimer.
* Record the CC0-1.0 option and provenance for pq-crystals/kyber-derived
ML-KEM portions, the BSD-3-Clause curve25519-donna origin of native X25519
portions, and the public-domain LZMA SDK origin of the x86 BCJ code.
* Keep AppImage outside the 5.2.2 promoted set until its static runtime has a
complete license/source-relink handoff; publish Windows only as a ZIP with
the executable and notices.
* Gate notice-bearing Linux tar.xz and Windows/macOS CLI bundles plus GUI
DEB, noarch RPM, source RPM, and source-only portable ZIP artifacts; keep
bare executables out of the promoted set.
* Qualify historical formal-verification and constant-time wording: current
source review and runtime regressions are not a proof for every compiler,
CPU, or final package binary.
-------------------------------------------------------------------
Fri Jul 10 18:00:00 UTC 2026 - Alessandro de Oliveira Faria <cabelo@opensuse.org>
@ -244,10 +310,10 @@ Tue May 26 02:27:34 UTC 2026 - Alessandro de Oliveira Faria <cabelo@opensuse.org
Tue May 26 00:43:52 UTC 2026 - Alessandro de Oliveira Faria <cabelo@opensuse.org>
- Update to 3.0.1
* GUI license cleanup: removed MIT credit line from the about
panel; gui/LICENSE-GUI replaced (was MIT) with AGPL-3.0-or-later
to match the source SPDX header. The GUI was never actually
released under MIT — that was a templating mistake.
* GUI license metadata changed to AGPL-3.0-or-later for the then-current
source. The original entry incorrectly denied earlier MIT grants; the
5.2.2 erratum records that they remain valid for the exact historical
material distributed under them.
* GUI version-string parsing bug fix (the replace("zupt ", ...)
substring also matched inside the v3.0.0 parenthetical). Window
title, splash header, status bar and about-panel hero number now
@ -296,7 +362,7 @@ Sun May 24 13:08:04 UTC 2026 - Alessandro de Oliveira Faria <cabelo@opensuse.org
OBS %check (no `make clean` mid-stream, no threading-flaky
tests). Spec now calls `make check` on x86_64/aarch64.
* License field corrected: AGPL-3.0-or-later (was MIT in 1.5.x).
Project is dual-licensed AGPL-3.0-or-later + commercial.
Commercial-terms inquiry information was documented separately.
* Upstream URL updated to git.securityops.co.
-------------------------------------------------------------------

View file

@ -0,0 +1,83 @@
#
# spec file for package zupt
#
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (c) 2026 SUSE LLC
# Copyright (c) 2026 Alessandro de Oliveira Faria (A.K.A. Cabelo) <cabelo@opensuse.org>
# Alessandro's attribution is for downstream openSUSE/OBS packaging only.
# Copyright (c) 2025-2026 Cristian Cezar Moisés <sac@securityops.co> (upstream)
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
# upon. The license for this file, and modifications and additions to the
# file, is the same license as for the pristine package itself (unless the
# license for the pristine package is not an Open Source License, in which
# case the license is the MIT License). An "Open Source License" is a
# license that conforms to the Open Source Definition (Version 1.9)
# published by the Open Source Initiative.
#
Name: zupt
Version: 5.2.2
Release: 0
Summary: Backup compression with authenticated and post-quantum encryption
License: AGPL-3.0-or-later AND GPL-3.0-or-later AND BSD-2-Clause AND BSD-3-Clause AND CC0-1.0
URL: https://github.com/cristiancmoises/zupt
Source0: %{name}-%{version}.tar.gz
BuildRequires: bash
BuildRequires: coreutils
BuildRequires: diffutils
BuildRequires: file
BuildRequires: findutils
BuildRequires: gawk
BuildRequires: gcc
BuildRequires: git-core
BuildRequires: grep
BuildRequires: gzip
BuildRequires: make
BuildRequires: python3-base
BuildRequires: sed
BuildRequires: tar
Provides: bundled(vaptvupt-codec) = 2.65.3
Provides: vaptvupt = %{version}-%{release}
Obsoletes: vaptvupt < %{version}
%description
ZUPT creates compressed backup archives with optional authenticated
password encryption or ML-KEM-768 and X25519 hybrid key encapsulation. The
default package is built entirely from the source in the release archive.
Optional SDK and PQBOX features are disabled because audited development
packages are unavailable. No private compiled library is installed.
%prep
%autosetup -p1
bash scripts/check-source-only.sh --tree .
%build
%make_build WITH_SDK=0 WITH_PQBOX=0 \
CFLAGS="%{optflags} -fPIE" \
LDFLAGS="%{?build_ldflags} -Wl,-z,relro,-z,now -pie"
%check
%make_build WITH_SDK=0 WITH_PQBOX=0 \
CFLAGS="%{optflags} -fPIE" \
LDFLAGS="%{?build_ldflags} -Wl,-z,relro,-z,now -pie" \
check
%install
%make_install WITH_SDK=0 WITH_PQBOX=0 INSTALL_LEGACY_ALIAS=0 INSTALL_LICENSES=0 \
PREFIX=%{_prefix} \
BINDIR=%{_bindir} \
MANDIR=%{_mandir}
%files
%license LICENSE LICENSE-AGPL-3.0 LICENSE-GPL-3.0 LICENSE-BSD-2-Clause LICENSE-BSD-3-Clause LICENSE-CC0-1.0 NOTICE THIRD-PARTY-NOTICES.md
%doc README.md CHANGELOG.md SECURITY.md THREAT_MODEL.md
%{_bindir}/zupt
%{_datadir}/bash-completion/completions/zupt
%{_datadir}/zsh/site-functions/_zupt
%{_datadir}/fish/vendor_completions.d/zupt.fish
%{_mandir}/man1/zupt.1%{?ext_man}
%changelog

View file

@ -1,57 +1,59 @@
VaptVupt GUI — portable cross-platform package
==============================================
ZUPT GUI — source-only portable launcher template
=====================================================
The VaptVupt GUI is a single Python file (zupt_gui.py) built on Qt for Python
(PySide6, or PyQt6 as a fallback). It runs on Windows, macOS, Linux and the
BSDs — anywhere Python 3 and a Qt binding are installed. This portable package
contains the GUI plus a launcher for each platform; it drives the `vaptvupt`
command-line tool under the hood.
This tracked directory contains three launcher templates and this assembly
guide; it is not a complete bundle by itself. A downstream source-only bundle
may add the integrated Python GUI source and artwork listed below, together
with the required license/provenance files. It must not contain Python, Qt, a
precompiled ZUPT command, or a vendored library. Its presence in a release
would not be evidence that every target operating system was tested; consult
that release's validation matrix.
Contents
--------
zupt_gui.py The GUI (PySide6 / PyQt6).
vaptvupt-gui.bat Windows launcher.
vaptvupt-gui.command macOS launcher (double-clickable in Finder).
vaptvupt-gui.sh Linux / BSD launcher.
assets/zupt-icon.png Application icon.
zupt_gui.py GUI source module (the historical module filename is
retained internally for source compatibility).
zupt-gui.bat Windows launcher.
zupt-gui.command macOS Finder launcher.
zupt-gui.sh POSIX shell launcher.
assets/zupt-icon.png PNG application artwork.
assets/zupt.ico Windows application artwork.
LICENSE-AGPL-3.0 Complete current GUI source license text.
LICENSE-GUI GUI licensing and historical-license note.
ASSET-PROVENANCE.md Artwork purpose, provenance, and license record.
CHANGELOG.md Release history and current compatibility notes.
Requirements
------------
1. Python 3.8 or newer.
Windows: https://python.org (tick "Add python.exe to PATH")
macOS: python.org, or `brew install python`
Linux: your distro's python3 package
FreeBSD: pkg install python311
OpenBSD: pkg_add python%3
2. A Qt binding:
pip (any OS): python3 -m pip install PySide6
Debian/Ubuntu: sudo apt install python3-pyqt6
Fedora/RHEL: sudo dnf install python3-pyqt6
FreeBSD: pkg install py311-pyside6
OpenBSD: pkg_add py3-pyside6
3. The vaptvupt CLI, either:
* placed next to the launcher (vaptvupt.exe on Windows, vaptvupt
elsewhere) — the launcher auto-detects it via VAPTVUPT_BIN, or
* installed on PATH (deb/rpm/AppImage/Homebrew/pkg — see the project
release page).
1. Python 3.9 or newer.
2. PySide6 6.5 or newer, or a compatible PyQt6 package.
3. ZUPT 5.2.2, installed as `zupt` on PATH or placed beside the launcher
(`zupt.exe` on Windows). A local command must have been built
and tested independently; this bundle never downloads one.
Running
-------
Windows: double-click vaptvupt-gui.bat
macOS: double-click vaptvupt-gui.command
(first run: right-click > Open to bypass Gatekeeper for an
unsigned script, or `xattr -dr com.apple.quarantine .`)
Linux/BSD: ./vaptvupt-gui.sh
Windows: zupt-gui.bat
macOS: zupt-gui.command
POSIX: ./zupt-gui.sh
The launchers set ZUPT_BIN when a local command is present. The GUI then
checks `zupt version`, discovers native and optional capabilities, and
exposes SDK or PQ-box modes only when the command reports the corresponding
system-library integration enabled.
Troubleshooting
---------------
* "requires PySide6 or PyQt6" -> install a Qt binding (requirement 2).
* "vaptvupt not found" -> put the CLI next to the launcher or on PATH.
* Set VAPTVUPT_DEBUG=1 to print the binary-discovery log to stderr.
* "requires PySide6 or PyQt6": install one Qt binding through your operating
system package manager or another trusted, preconfigured Python source.
* "zupt not found": install ZUPT 5.2.2 or place its command beside
the launcher.
* Set ZUPT_DEBUG=1 to print command-discovery diagnostics to stderr.
Fully self-contained native installers (Windows .exe/.msi, macOS .dmg) that
bundle Python + Qt + the CLI are produced by the project's CI on real Windows
and macOS runners — see the release page. This portable package is the
dependency-light option that works identically on every platform.
The old user-facing command name is not installed by this bundle. The `.zupt`
archive extension remains unchanged for format compatibility.
License: AGPL-3.0-or-later. Project: https://git.securityops.co/cristiancmoises/vaptvupt
Current GUI source license: AGPL-3.0-or-later. Published historical revisions
include MIT grants for the exact material covered by their notices; see
LICENSE-GUI and the 5.2.2 erratum in CHANGELOG.md.
Project: https://github.com/cristiancmoises/zupt

View file

@ -1,17 +0,0 @@
#!/bin/bash
# SPDX-License-Identifier: AGPL-3.0-or-later
# VaptVupt GUI launcher for macOS (portable package).
# Double-clickable in Finder (.command). Requirements on the target Mac:
# * Python 3.8+ (python.org, Homebrew `brew install python`, or Xcode CLT)
# * PySide6 or PyQt6: python3 -m pip install PySide6
# * The vaptvupt CLI: `vaptvupt` next to this file, or on PATH
# (Homebrew: `brew install cristiancmoises/tap/vaptvupt`).
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
[ -x "$HERE/vaptvupt" ] && export VAPTVUPT_BIN="$HERE/vaptvupt"
PY="$(command -v python3 || command -v python || true)"
if [ -z "$PY" ]; then
osascript -e 'display alert "VaptVupt GUI" message "Python 3 not found. Install it from python.org or `brew install python`, then run: python3 -m pip install PySide6"' 2>/dev/null
echo "Python 3 not found." >&2; exit 1
fi
exec "$PY" "$HERE/zupt_gui.py" "$@"

View file

@ -1,17 +1,17 @@
@echo off
rem SPDX-License-Identifier: AGPL-3.0-or-later
rem VaptVupt GUI launcher for Windows (portable package).
rem ZUPT GUI launcher for Windows (portable package).
rem
rem Requirements on the target machine:
rem * Python 3.8+ (https://python.org — tick "Add python.exe to PATH")
rem * Python 3.9+
rem * PySide6 or PyQt6: py -m pip install PySide6
rem * The vaptvupt CLI: vaptvupt.exe next to this file, or on PATH.
rem * The ZUPT CLI: zupt.exe next to this file, or on PATH.
rem
rem If vaptvupt.exe sits beside this launcher we pin it via VAPTVUPT_BIN so the
rem If zupt.exe sits beside this launcher we pin it via ZUPT_BIN so the
rem GUI drives the bundled CLI rather than any other copy on PATH.
setlocal
set "HERE=%~dp0"
if exist "%HERE%vaptvupt.exe" set "VAPTVUPT_BIN=%HERE%vaptvupt.exe"
if exist "%HERE%zupt.exe" set "ZUPT_BIN=%HERE%zupt.exe"
rem Prefer the py launcher, fall back to python on PATH.
where py >nul 2>nul
@ -23,9 +23,8 @@ if %ERRORLEVEL%==0 (
set "RC=%ERRORLEVEL%"
if not "%RC%"=="0" (
echo.
echo vaptvupt-gui exited with code %RC%.
echo zupt-gui exited with code %RC%.
echo If you saw an import error, install the Qt binding: py -m pip install PySide6
echo If the CLI was not found, put vaptvupt.exe next to this launcher or on PATH.
pause
echo If the CLI was not found, put zupt.exe next to this launcher or on PATH.
)
endlocal
endlocal & exit /b %RC%

View file

@ -0,0 +1,17 @@
#!/bin/bash
# SPDX-License-Identifier: AGPL-3.0-or-later
# ZUPT GUI launcher for macOS (portable package).
# Double-clickable in Finder (.command). Requirements on the target Mac:
# * Python 3.9+
# * PySide6 or PyQt6: python3 -m pip install PySide6
# * The ZUPT CLI: `zupt` next to this file, or on PATH
# (Homebrew: `brew install cristiancmoises/tap/zupt`).
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
[ -x "$HERE/zupt" ] && export ZUPT_BIN="$HERE/zupt"
PY="$(command -v python3 || command -v python || true)"
if [ -z "$PY" ]; then
osascript -e 'display alert "ZUPT GUI" message "Python 3.9 or newer was not found. Install Python and a trusted PySide6 or PyQt6 package."' 2>/dev/null
echo "Python 3 not found." >&2; exit 1
fi
exec "$PY" "$HERE/zupt_gui.py" "$@"

View file

@ -1,21 +1,21 @@
#!/bin/sh
# SPDX-License-Identifier: AGPL-3.0-or-later
# VaptVupt GUI launcher for Linux and the BSDs (portable package).
# ZUPT GUI launcher for Linux and the BSDs (portable package).
# Requirements on the target system:
# * Python 3.8+
# * Python 3.9+
# * PySide6 or PyQt6
# Debian/Ubuntu: sudo apt install python3-pyqt6
# Fedora/RHEL: sudo dnf install python3-pyqt6
# FreeBSD: pkg install py311-pyside6 (or py311-qt6-pyqt)
# OpenBSD: pkg_add py3-pyside6
# any OS via pip: python3 -m pip install PySide6
# * The vaptvupt CLI: `vaptvupt` next to this file, or on PATH.
# * The ZUPT CLI: `zupt` next to this file, or on PATH.
HERE="$(cd "$(dirname "$0")" && pwd)"
[ -x "$HERE/vaptvupt" ] && export VAPTVUPT_BIN="$HERE/vaptvupt"
[ -x "$HERE/zupt" ] && export ZUPT_BIN="$HERE/zupt"
PY="$(command -v python3 || command -v python || true)"
if [ -z "$PY" ]; then
echo "vaptvupt-gui: Python 3 not found on PATH." >&2
echo "zupt-gui: Python 3 not found on PATH." >&2
exit 1
fi
exec "$PY" "$HERE/zupt_gui.py" "$@"

View file

@ -1,72 +1,70 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# Fedora / RHEL / CentOS RPM spec for vaptvupt.
# Fedora / RHEL / CentOS RPM spec for zupt.
#
# Build with:
# spectool -g vaptvupt.spec # fetches the upstream tarball
# rpmbuild -ba vaptvupt.spec # builds source + binary RPMs
# spectool -g zupt.spec # fetches the upstream tarball
# rpmbuild -ba zupt.spec # builds source + binary RPMs
#
# To bring a release into production:
# 1. Run `make dist` upstream → /tmp/vaptvupt-VERSION.tar.gz (reproducible).
# 2. Upload to a stable release URL (git.securityops.co releases).
# 1. Run `make dist` upstream → /tmp/zupt-VERSION.tar.gz (reproducible).
# 2. Upload to the canonical GitHub release.
# 3. Update %{version} below.
# 4. Run `sha256sum /tmp/vaptvupt-VERSION.tar.gz` and update Source0
# 4. Run `sha256sum /tmp/zupt-VERSION.tar.gz` and update Source0
# checksum (handled by spectool when configured) or pin via
# sha256sum in a separate manifest if your distro requires it.
# 5. rpmbuild --define '_topdir ~/rpmbuild' -ba zupt.spec
# 5. rpmbuild --define '_topdir /path/to/rpmbuild' -ba zupt.spec
#
# This spec is written for Fedora 38+ and EPEL 9+; it should also work
# on RHEL 8 (with EPEL) by adjusting BuildRequires if Python 3.8+ isn't
# in the base.
# This is an upstream Fedora-family recipe. A target is supported only after
# that exact distribution release and architecture have built and passed the
# installed smoke test.
Name: vaptvupt
Version: 5.2.1
Name: zupt
Version: 5.2.2
Release: 1%{?dist}
Summary: Post-quantum backup compression utility (AES-256 + ML-KEM-768 + Argon2id, formerly Zupt)
Summary: Backup compression with authenticated and post-quantum encryption
License: AGPL-3.0-or-later AND GPL-3.0-or-later
URL: https://git.securityops.co/cristiancmoises/vaptvupt
License: AGPL-3.0-or-later AND GPL-3.0-or-later AND BSD-2-Clause AND BSD-3-Clause AND CC0-1.0
URL: https://github.com/cristiancmoises/zupt
Source0: %{url}/releases/download/v%{version}/%{name}-%{version}.tar.gz
# v3.0.0: legacy `zupt` package is superseded. Renaming was forced
# by a prior INPI Brasil trademark registration on "Zupt". The
# archive extension (.zupt), wire format, magic bytes, and C ABI
# are unchanged.
Provides: zupt = %{version}-%{release}
Obsoletes: zupt < 3.0.0
Conflicts: zupt < 3.0.0
BuildRequires: gcc
BuildRequires: git-core
BuildRequires: make
BuildRequires: glibc-devel
BuildRequires: python3 >= 3.8
BuildRequires: bash
BuildRequires: coreutils
BuildRequires: diffutils
BuildRequires: file
BuildRequires: findutils
BuildRequires: gawk
BuildRequires: grep
BuildRequires: gzip
BuildRequires: sed
BuildRequires: tar
# python3 is only needed for the regression-test harness (byte sweeps,
# tamper injection). The shipped binary has no Python dependency.
Requires: glibc
Provides: bundled(vaptvupt-codec) = 2.65.3
%description
Zupt is a pure-C11 backup compression utility featuring:
ZUPT is a pure-C11 backup compression utility featuring:
* Post-quantum hybrid encryption (ML-KEM-768 + X25519, FIPS 203,
validated byte-for-byte against OpenSSL's ML-KEM-768) and full
pure ML-KEM-768 (--pq-only)
* Post-quantum hybrid encryption (ML-KEM-768 + X25519) and full
ML-KEM-768 mode (--pq-only)
* AES-256-CTR + HMAC-SHA256 authenticated encryption (Encrypt-then-MAC)
* PBKDF2-SHA256 password key derivation (Argon2id in WITH_SDK=1 builds)
* Multi-threaded compression with the VaptVupt LZ + ANS codec
* Full-disk backup and restore with sparse-region detection
* End-to-end byte-level tamper detection on encrypted archives
(0 silent-accept positions in the v1.6 exhaustive byte sweep)
* Constant-time cryptographic primitives verified with Jasmin
* Authenticated encrypted-archive metadata and per-block integrity checks
* Portable C implementations with optional source-built assembly paths
* NIST/RFC test vectors for SHA-256, SHA-3, ML-KEM-768, AES-256-CTR,
HMAC-SHA256, X25519, PBKDF2, Argon2id
HMAC-SHA256, X25519 and PBKDF2
The archive format includes an integrity trailer that authenticates
the header and footer, per-block HMAC with bound frame-preface AAD,
and optional encrypted comments.
%global debug_package %{nil}
# Single source RPM, no -debuginfo split for the initial release.
Encrypted archives include an integrity trailer that authenticates the header
and footer, per-block HMAC with bound frame-preface AAD, and optional encrypted
comments. Plain archives use non-cryptographic checksums.
%prep
%autosetup -n %{name}-%{version}
@ -74,41 +72,41 @@ and optional encrypted comments.
%build
# Source-only build (WITH_SDK=0): no vendored libraries, no external crypto
# dependency. Fedora's default optflags plus the project's warning set.
%make_build WITH_SDK=0 \
CFLAGS="%{optflags} -fPIE -Wall -Wextra -std=c11 -Iinclude -Isrc" \
LDFLAGS="%{?build_ldflags} -pie" \
LDLIBS="-lm -lpthread"
%make_build V=1 WITH_SDK=0 WITH_PQBOX=0 \
CFLAGS="%{optflags}" \
LDFLAGS="%{?build_ldflags}"
%check
# Distro-safe regression subset: F-06 HMAC trials, F-08 top-MAC sweep,
# F-09 byte sweep, F-10..F-12 regressions, the dedup-nonce regression,
# and NIST/RFC vectors. Skips threaded/dist-reproducibility tests that
# are sensitive to the build host.
%make_build WITH_SDK=0 \
CFLAGS="%{optflags} -fPIE -Wall -Wextra -std=c11 -Iinclude -Isrc" \
LDFLAGS="%{?build_ldflags} -pie" \
LDLIBS="-lm -lpthread" \
# Distro-safe quick, path-traversal, integrity, codec, HMAC and NIST/RFC
# checks. Full, optional-integration and dist-reproducibility suites remain
# release gates outside the package build.
%make_build V=1 WITH_SDK=0 WITH_PQBOX=0 \
CFLAGS="%{optflags}" \
LDFLAGS="%{?build_ldflags}" \
check
%install
%make_install WITH_SDK=0 DESTDIR=%{buildroot} PREFIX=/usr
%make_install WITH_SDK=0 WITH_PQBOX=0 INSTALL_LEGACY_ALIAS=0 INSTALL_LICENSES=0 \
PREFIX=%{_prefix} BINDIR=%{_bindir} MANDIR=%{_mandir}
%files
%license LICENSE
%doc README.md SECURITY.md CHANGELOG.md
%license LICENSE LICENSE-AGPL-3.0 LICENSE-GPL-3.0 LICENSE-BSD-2-Clause LICENSE-BSD-3-Clause LICENSE-CC0-1.0 NOTICE THIRD-PARTY-NOTICES.md
%doc README.md SECURITY.md THREAT_MODEL.md CHANGELOG.md
%{_bindir}/%{name}
%{_bindir}/zupt
%{_datadir}/bash-completion/completions/%{name}
%{_datadir}/bash-completion/completions/zupt
%{_datadir}/zsh/site-functions/_%{name}
%{_datadir}/zsh/site-functions/_zupt
%{_datadir}/fish/vendor_completions.d/%{name}.fish
%if 0%{?_mandir:1}
%{_mandir}/man1/%{name}.1*
%{_mandir}/man1/zupt.1*
%endif
%changelog
* Mon Aug 31 2026 Cristian Cezar Moisés <sac@securityops.co> - 5.2.2-1
- Source-only release; optional SDK/PQBOX integrations use system development
packages only and are disabled for this package.
- Preserve distribution flags and debuginfo, remove RPATH/vendor-library
fallbacks, run the real upstream check target, and restore the zupt command.
* Sat Jul 11 2026 Cristian Cezar Moisés <sac@securityops.co> - 5.1.0-1
- Codec 2.65.0; large compression-ratio gains (auto format_v2 + level-scaled
block window); --dedup keeps a small block; GUI compress-hang and

View file

@ -1,73 +0,0 @@
; SPDX-License-Identifier: AGPL-3.0-or-later
; Inno Setup script for the VaptVupt GUI Windows installer.
;
; Compiled by the cross-platform CI (.github/workflows/cross-platform.yml) with:
; ISCC.exe /DAppVersion=<version> packaging/windows/vaptvupt-gui.iss
; after PyInstaller has produced dist\vaptvupt-gui.exe (a onefile bundle that
; already contains Python, PySide6 and vaptvupt.exe). Requires Inno Setup 6+.
;
; To build locally on Windows: install Inno Setup, then run the same ISCC line
; from the repo root (with dist\vaptvupt-gui.exe present).
#ifndef AppVersion
#define AppVersion "0.0.0"
#endif
[Setup]
AppName=VaptVupt
AppVersion={#AppVersion}
AppPublisher=Cristian Cezar Moises
AppPublisherURL=https://git.securityops.co/cristiancmoises/vaptvupt
DefaultDirName={autopf}\VaptVupt
DefaultGroupName=VaptVupt
UninstallDisplayIcon={app}\vaptvupt-gui.exe
OutputDir=packaging\windows\Output
OutputBaseFilename=VaptVupt-Setup-{#AppVersion}
Compression=lzma2
SolidCompression=yes
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
WizardStyle=modern
LicenseFile=LICENSE
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Files]
; PyInstaller onefile bundle (Python + PySide6 + the GUI + vaptvupt.exe).
Source: "dist\vaptvupt-gui.exe"; DestDir: "{app}"; Flags: ignoreversion
; Ship the raw CLI too so it can be added to PATH and used from a terminal.
Source: "vaptvupt.exe"; DestDir: "{app}"; Flags: ignoreversion skipifsourcedoesntexist
Source: "README.md"; DestDir: "{app}"; Flags: ignoreversion isreadme
Source: "CHANGELOG.md"; DestDir: "{app}"; Flags: ignoreversion
[Icons]
Name: "{group}\VaptVupt"; Filename: "{app}\vaptvupt-gui.exe"
Name: "{group}\Uninstall VaptVupt"; Filename: "{uninstallexe}"
Name: "{autodesktop}\VaptVupt"; Filename: "{app}\vaptvupt-gui.exe"; Tasks: desktopicon
[Tasks]
Name: "desktopicon"; Description: "Create a desktop shortcut"; GroupDescription: "Additional icons:"
Name: "addtopath"; Description: "Add the vaptvupt CLI to PATH (current user)"; GroupDescription: "Command line:"
[Registry]
; Optionally add the install dir to the user PATH (for the vaptvupt.exe CLI).
Root: HKCU; Subkey: "Environment"; ValueType: expandsz; ValueName: "Path"; \
ValueData: "{olddata};{app}"; Tasks: addtopath; Check: NeedsAddPath('{app}')
[Run]
Filename: "{app}\vaptvupt-gui.exe"; Description: "Launch VaptVupt"; \
Flags: nowait postinstall skipifsilent
[Code]
function NeedsAddPath(Param: string): Boolean;
var
OrigPath: string;
begin
if not RegQueryStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', OrigPath) then
begin
Result := True;
exit;
end;
Result := Pos(';' + ExpandConstant(Param) + ';', ';' + OrigPath + ';') = 0;
end;

View file

@ -0,0 +1,98 @@
; SPDX-License-Identifier: AGPL-3.0-or-later
; Inno Setup 6 recipe for target-built ZUPT Windows artifacts.
;
; All paths are mandatory command-line definitions. This prevents the recipe
; from silently picking up a stale or placeholder executable from the tree.
#ifndef AppVersion
#error AppVersion must be defined
#endif
#ifndef GuiExecutable
#error GuiExecutable must name a tested PyInstaller GUI executable
#endif
#ifndef CliExecutable
#error CliExecutable must name a tested source-built zupt.exe
#endif
#ifndef BuildOutputDir
#error BuildOutputDir must be an external output directory
#endif
#ifndef RuntimeNoticesDir
#error RuntimeNoticesDir must contain notices for the exact bundled GUI runtime
#endif
[Setup]
AppId={{59AD35E4-1860-445D-8E89-4563DB9ED4E2}
AppName=ZUPT
AppVersion={#AppVersion}
AppPublisher=Cristian Cezar Moises
AppPublisherURL=https://github.com/cristiancmoises/zupt
AppSupportURL=https://github.com/cristiancmoises/zupt/issues
DefaultDirName={autopf}\ZUPT
DefaultGroupName=ZUPT
UninstallDisplayIcon={app}\zupt-gui.exe
OutputDir={#BuildOutputDir}
OutputBaseFilename=ZUPT-Setup-{#AppVersion}
Compression=lzma2
SolidCompression=yes
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
WizardStyle=modern
LicenseFile=..\..\LICENSE
ChangesAssociations=yes
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Files]
Source: "{#GuiExecutable}"; DestDir: "{app}"; DestName: "zupt-gui.exe"; Flags: ignoreversion
Source: "{#CliExecutable}"; DestDir: "{app}"; DestName: "zupt.exe"; Flags: ignoreversion
Source: "..\..\LICENSE"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\..\LICENSE-AGPL-3.0"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\..\LICENSE-GPL-3.0"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\..\LICENSE-BSD-2-Clause"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\..\LICENSE-BSD-3-Clause"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\..\LICENSE-CC0-1.0"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\..\NOTICE"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\..\THIRD-PARTY-NOTICES.md"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\..\gui\LICENSE-GUI"; DestDir: "{app}"; Flags: ignoreversion
Source: "..\..\gui\assets\README.md"; DestDir: "{app}"; DestName: "GUI-ASSET-PROVENANCE.md"; Flags: ignoreversion
Source: "{#RuntimeNoticesDir}\*"; DestDir: "{app}\third-party-runtime-notices"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "..\..\README.md"; DestDir: "{app}"; Flags: ignoreversion isreadme
Source: "..\..\CHANGELOG.md"; DestDir: "{app}"; Flags: ignoreversion
[Icons]
Name: "{group}\ZUPT GUI"; Filename: "{app}\zupt-gui.exe"
Name: "{group}\ZUPT command prompt"; Filename: "{cmd}"; Parameters: "/K cd /d ""{app}"""
Name: "{group}\Uninstall ZUPT"; Filename: "{uninstallexe}"
Name: "{autodesktop}\ZUPT GUI"; Filename: "{app}\zupt-gui.exe"; Tasks: desktopicon
[Tasks]
Name: "desktopicon"; Description: "Create a desktop shortcut"; GroupDescription: "Additional icons:"
Name: "addtopath"; Description: "Add the ZUPT command to PATH for this user"; GroupDescription: "Command line:"
[Registry]
Root: HKCU; Subkey: "Environment"; ValueType: expandsz; ValueName: "Path"; \
ValueData: "{olddata};{app}"; Tasks: addtopath; Check: NeedsAddPath('{app}')
Root: HKCU; Subkey: "Software\Classes\.zupt"; ValueType: string; ValueName: ""; \
ValueData: "ZUPT.Archive"; Flags: uninsdeletevalue
Root: HKCU; Subkey: "Software\Classes\ZUPT.Archive"; ValueType: string; \
ValueName: ""; ValueData: "ZUPT archive"; Flags: uninsdeletekey
Root: HKCU; Subkey: "Software\Classes\ZUPT.Archive\shell\open\command"; \
ValueType: string; ValueName: ""; ValueData: """{app}\zupt-gui.exe"" --extract ""%1"""
[Run]
Filename: "{app}\zupt-gui.exe"; Description: "Launch ZUPT GUI"; \
Flags: nowait postinstall skipifsilent
[Code]
function NeedsAddPath(Param: string): Boolean;
var
OrigPath: string;
begin
if not RegQueryStringValue(HKEY_CURRENT_USER, 'Environment', 'Path', OrigPath) then
begin
Result := True;
exit;
end;
Result := Pos(';' + ExpandConstant(Param) + ';', ';' + OrigPath + ';') = 0;
end;

View file

@ -1,312 +0,0 @@
#!/bin/bash
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (c) 2025-2026 Cristian Cezar Moisés
# ╔════════════════════════════════════════════════════════════════════╗
# ║ ZUPT 2.2.3 + 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.3"
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.3-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