ci: build Linux release packages

This commit is contained in:
2026-07-17 02:08:57 +02:00
parent 635e7268b5
commit d0844068bb
5 changed files with 563 additions and 1 deletions

84
packaging/linux/README.md Normal file
View File

@@ -0,0 +1,84 @@
# Linux packaging
VniDrop ships native x64 packages for the two common Linux package families:
- Debian/Ubuntu: `.deb`
- Current Fedora systems: `.rpm`
Both packages contain the application, its release Rust library, and a private
Java runtime. Users do not need to install Java separately. The packages are
currently distributed as direct GitHub Release downloads, so they use SHA-256
checksums rather than a Linux repository signing key. A future APT or RPM
repository should add repository metadata signing and its own update channel.
## GitHub Actions
The Linux packages workflow runs for relevant pull requests, release tags
matching `vMAJOR.MINOR.PATCH`, and manual dispatches. Each native package is
built and validated on its matching distribution family:
- `.deb` on Ubuntu 22.04 for a conservative glibc baseline
- `.rpm` inside Fedora 43 so `jpackage` can discover normal RPM dependencies
The shared JVM suite runs inside the Debian build job. Package construction and
payload validation happen in both build jobs, so there is no separate test
runner. Pull requests build and verify both packages but do not retain
artifacts. Manual runs retain build artifacts for 14 days. A pushed version tag
whose commit is on `master` creates the matching GitHub Release with the `.deb`,
`.rpm`, and a combined `SHA256SUMS`.
The existing `v1.0.0` tag predates this workflow and will not run it
retroactively. Use the next version tag after this configuration reaches
`master`.
## Install a downloaded package
Verify downloads from the directory containing all three release files:
```bash
sha256sum -c SHA256SUMS
```
On Debian or Ubuntu:
```bash
sudo apt install ./vnidrop_VERSION-1_amd64.deb
```
On Fedora:
```bash
sudo dnf install ./vnidrop-VERSION-1.x86_64.rpm
```
The package manager installs declared system-library dependencies and creates
the VniDrop desktop launcher. Uninstall with `sudo apt remove vnidrop` or
`sudo dnf remove vnidrop`.
## Manual native builds
Use JDK 21 and Rust 1.91. Build DEB packages on Debian/Ubuntu with `dpkg` and
`fakeroot`; build RPM packages on Fedora with `rpm-build`. Building an RPM on
Ubuntu prevents `jpackage` from discovering normal RPM dependencies.
From the repository root on the matching Linux family, run one of:
```bash
./gradlew :shared:jvmTest :desktopApp:packageReleaseDeb \
-Pvnidrop.version=1.0.0 \
-Pvnidrop.desktop.rustVariant=release \
-Pvnidrop.diagnostics.included=false \
--no-daemon --no-configuration-cache --stacktrace
./gradlew :shared:jvmTest :desktopApp:packageReleaseRpm \
-Pvnidrop.version=1.0.0 \
-Pvnidrop.desktop.rustVariant=release \
-Pvnidrop.diagnostics.included=false \
--no-daemon --no-configuration-cache --stacktrace
```
Compose writes the packages under
`desktopApp/build/compose/binaries/main-release/deb/` and
`desktopApp/build/compose/binaries/main-release/rpm/`. The workflow then
validates package identity, version, architecture, dependencies, bundled JVM,
and release Rust payload before publishing anything.

View File

@@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -euo pipefail
version=${1:-1.0.0}
if [[ ${GITHUB_REF_TYPE:-} == "tag" ]]; then
if [[ ! ${GITHUB_REF_NAME:-} =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Linux release tags must use vMAJOR.MINOR.PATCH" >&2
exit 1
fi
version=${GITHUB_REF_NAME#v}
fi
if [[ ! $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Version must use MAJOR.MINOR.PATCH" >&2
exit 1
fi
IFS=. read -r major minor patch <<< "$version"
parts=("$major" "$minor" "$patch")
for index in "${!parts[@]}"; do
part=${parts[$index]}
if [[ $part != "0" && $part == 0* ]]; then
echo "Version components must be canonical integers without leading zeroes" >&2
exit 1
fi
if (( ${#part} > 5 )) || (( 10#$part > 65535 )); then
echo "Version components must be between 0 and 65535" >&2
exit 1
fi
if (( index == 0 && 10#$part == 0 )); then
echo "The major version must be non-zero" >&2
exit 1
fi
done
printf '%s\n' "$version"

View File

@@ -0,0 +1,96 @@
#!/usr/bin/env bash
set -euo pipefail
if (( $# != 3 )); then
echo "Usage: $0 <deb|rpm> <MAJOR.MINOR.PATCH> <package-path>" >&2
exit 2
fi
format=$1
version=$2
package_path=$3
fail() {
echo "Package verification failed: $*" >&2
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || fail "required command '$1' is not installed"
}
[[ -f $package_path ]] || fail "package not found: $package_path"
require_command find
require_command unzip
extract_root=$(mktemp -d)
trap 'rm -rf "$extract_root"' EXIT
case $format in
deb)
require_command dpkg-deb
[[ $(dpkg-deb -f "$package_path" Package) == "vnidrop" ]] || fail "unexpected Debian package name"
[[ $(dpkg-deb -f "$package_path" Version) == "$version-1" ]] || fail "unexpected Debian package version"
[[ $(dpkg-deb -f "$package_path" Architecture) == "amd64" ]] || fail "unexpected Debian architecture"
[[ $(dpkg-deb -f "$package_path" Maintainer) == *"support@sudosy.fr"* ]] || fail "unexpected Debian maintainer"
deb_dependencies=$(dpkg-deb -f "$package_path" Depends)
deb_libc_pattern='(^|,[[:space:]])libc6([[:space:](]|,|$)'
deb_xdg_pattern='(^|,[[:space:]])xdg-utils([[:space:](]|,|$)'
[[ $deb_dependencies =~ $deb_libc_pattern ]] || fail "Debian package does not declare libc6"
[[ $deb_dependencies =~ $deb_xdg_pattern ]] || fail "Debian package does not declare xdg-utils"
dpkg-deb -x "$package_path" "$extract_root"
;;
rpm)
require_command rpm
require_command rpm2cpio
require_command cpio
metadata=$(rpm -qp --queryformat '%{NAME}\n%{VERSION}\n%{RELEASE}\n%{ARCH}\n%{LICENSE}\n' "$package_path")
mapfile -t fields <<< "$metadata"
[[ ${fields[0]:-} == "vnidrop" ]] || fail "unexpected RPM package name"
[[ ${fields[1]:-} == "$version" ]] || fail "unexpected RPM package version"
[[ ${fields[2]:-} == "1" ]] || fail "unexpected RPM release"
[[ ${fields[3]:-} == "x86_64" ]] || fail "unexpected RPM architecture"
[[ ${fields[4]:-} == "Apache-2.0" ]] || fail "unexpected RPM license"
mapfile -t runtime_requirements < <(rpm -qpR "$package_path" | grep -Ev '^(rpmlib\(|/bin/sh$)' || true)
printf '%s\n' "${runtime_requirements[@]}" | grep -Eq '^(glibc($|[[:space:]])|libc\.so\.6)' || fail "RPM package does not declare glibc"
printf '%s\n' "${runtime_requirements[@]}" | grep -Fxq 'xdg-utils' || fail "RPM package does not declare xdg-utils"
rpm2cpio "$package_path" | (
cd "$extract_root"
cpio -idm --quiet
)
;;
*)
fail "unsupported package format: $format"
;;
esac
mapfile -d '' -t launchers < <(find "$extract_root" -type f -iname 'vnidrop' -perm /111 -print0)
(( ${#launchers[@]} == 1 )) || fail "expected exactly one executable VniDrop launcher"
mapfile -d '' -t desktop_entries < <(find "$extract_root" -type f -name '*.desktop' -print0)
(( ${#desktop_entries[@]} == 1 )) || fail "expected exactly one desktop entry"
grep -Eiq '^Exec=.*/VniDrop([[:space:]]|$)' "${desktop_entries[0]}" || fail "desktop entry does not launch VniDrop"
grep -Eq '^MimeType=.*application/vnd\.vnidrop\.transfer(;|$)' "${desktop_entries[0]}" || fail "desktop entry does not register VniDrop invitations"
mapfile -d '' -t bundled_jvms < <(find "$extract_root" -type f -path '*/lib/runtime/lib/server/libjvm.so' -print0)
(( ${#bundled_jvms[@]} == 1 )) || fail "expected exactly one bundled JVM"
mapfile -d '' -t debug_rust_jars < <(find "$extract_root" -type f -name 'shared-linux-x86-64-debug-*.jar' -print0)
(( ${#debug_rust_jars[@]} == 0 )) || fail "package contains a debug Rust runtime JAR"
mapfile -d '' -t rust_jars < <(
find "$extract_root" -type f -name 'shared-linux-x86-64-*.jar' ! -name 'shared-linux-x86-64-debug-*.jar' -print0
)
(( ${#rust_jars[@]} == 1 )) || fail "expected exactly one release Rust runtime JAR"
native_entry_size=$(unzip -p "${rust_jars[0]}" 'linux-x86-64/libvnidrop.so' | wc -c)
[[ $native_entry_size =~ ^[0-9]+$ ]] || fail "release Rust runtime JAR does not contain libvnidrop.so"
(( native_entry_size > 0 )) || fail "release Rust runtime JAR contains an empty libvnidrop.so"
mapfile -d '' -t shared_jars < <(find "$extract_root" -type f -name 'shared-jvm-*.jar' -print0)
(( ${#shared_jars[@]} == 1 )) || fail "expected exactly one shared JVM JAR"
unzip -p "${shared_jars[0]}" META-INF/MANIFEST.MF | tr -d '\r' |
grep -Fxq "Implementation-Version: $version" || fail "packaged app version does not match $version"
printf 'Verified %s package: %s\n' "$format" "$package_path"