Files
RCalc/scripts/build-deb.sh
T
alex 3c14906e40 deb packaging: build-deb.sh + packaging/deb, icons from ico/
- scripts/build-deb.sh: binary built in ubuntu:22.04 (glibc 2.35 baseline),
  Depends via dpkg-shlibdeps, DEB_REVISION (0.1.28-1), changelog.Debian.gz,
  postinst/postrm cache refresh, REPO=1 local apt index, self-checks
  (version/upgrade path, stale binary, desktop-file-validate)
- packaging/deb/: control.in, rcalc.desktop, appdata metainfo, changelog, hooks
- ico/: new app icon in 32/48/64/96/256/512, used for the hicolor slots
- convert.rs: fix clippy::approx_constant error in roundtrip test (PI constant)
- README: install/update/release instructions
2026-09-17 19:48:05 +03:00

440 lines
17 KiB
Bash
Executable File

#!/usr/bin/env bash
# Build a Debian package (.deb) for rcalc.
#
# By default the release binary is compiled inside Ubuntu 22.04 (glibc 2.35) —
# the same baseline and the same cargo cache (target-appimage) as
# scripts/build-appimage.sh — so the package installs on Ubuntu 22.04 / 24.04
# and not only on this rolling host distro.
#
# Package layout (FHS):
# /usr/bin/rcalc stripped release binary
# /usr/share/applications/rcalc.desktop launcher (Exec=rcalc)
# /usr/share/icons/hicolor/*x*/apps/rcalc.png icons from ico/ (or assets/)
# /usr/share/metainfo/rcalc.appdata.xml AppStream metadata
# /usr/share/doc/rcalc/README.md
#
# Env switches:
# DEB_REVISION packaging revision appended to the Cargo.toml version
# (0.1.28 + 1 → "Version: 0.1.28-1"); bump it for updates that
# change only the package, not the app
# REPO=1 also generate dist/Packages(.gz) → dist/ usable as an apt repo
# SKIP_DOCKER=1 build on the host instead of Docker (fast, but the deb will
# then require this host's glibc — see DEPENDS below)
# PREBUILT=1 do not compile, package the existing dist/rcalc as-is
# NO_STRIP=1 keep symbols in the binary (much bigger package)
# KEEP_STAGE=1 leave dist/deb-root/ around for inspection
# DOCKER_IMAGE build container image (default: ubuntu:22.04)
# DEB_MAINTAINER "Name <mail>" (default: git user.name/user.email)
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PKG="rcalc"
OUT_DIR="${OUT_DIR:-${ROOT}/dist}"
META_DIR="${ROOT}/packaging/deb"
STAGE="${OUT_DIR}/deb-root"
ARCH="$(dpkg --print-architecture 2>/dev/null || uname -m)"
VERSION="$(grep -m1 '^version' "${ROOT}/Cargo.toml" | sed 's/.*"\(.*\)".*/\1/')"
# Debian version = upstream (Cargo.toml) + packaging revision: "0.1.28-1".
# The revision lets you ship an update (fixed desktop file, new icon, rebuilt
# binary) WITHOUT touching Cargo.toml; dpkg compares 0.1.28-1 < 0.1.28-2 <
# 0.1.29-1 correctly. DEB_REVISION is the knob for that.
DEB_REVISION="${DEB_REVISION:-1}"
DEB_VERSION="${VERSION}-${DEB_REVISION}"
DEB_FILE="${OUT_DIR}/${PKG}_${DEB_VERSION}_${ARCH}.deb"
DOCKER_IMAGE="${DOCKER_IMAGE:-ubuntu:22.04}"
SKIP_DOCKER="${SKIP_DOCKER:-0}"
PREBUILT="${PREBUILT:-0}"
NO_STRIP="${NO_STRIP:-0}"
KEEP_STAGE="${KEEP_STAGE:-0}"
# Nested $( ) with quotes inside ${:- } is parsed unreliably by bash — resolve first.
GIT_NAME="$(git -C "${ROOT}" config user.name 2>/dev/null || true)"
GIT_MAIL="$(git -C "${ROOT}" config user.email 2>/dev/null || true)"
MAINTAINER="${DEB_MAINTAINER:-${GIT_NAME:-${PKG}-packaging} <${GIT_MAIL:-localhost}>}"
need_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "error: required command not found: $1" >&2
exit 1
}
}
# ---------------------------------------------------------------- binary ----
build_binary_docker() {
need_cmd docker
echo "Building release binary in ${DOCKER_IMAGE} (glibc 2.35 baseline)…"
mkdir -p "${HOME}/.cargo/registry" "${HOME}/.cargo/git"
docker run --rm \
-v "${ROOT}:/src" \
-v "${HOME}/.cargo/registry:/root/.cargo/registry" \
-v "${HOME}/.cargo/git:/root/.cargo/git" \
-w /src \
"${DOCKER_IMAGE}" \
bash -lc '
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq curl build-essential pkg-config libfontconfig1-dev ca-certificates
curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
# shellcheck disable=SC1091
source "$HOME/.cargo/env"
export CARGO_TARGET_DIR=/src/target-appimage
cargo build --release
install -D -m755 /src/target-appimage/release/rcalc /src/dist/rcalc
ldd --version | head -1 > /src/dist/.glibc-build.txt
# Make artifacts writable by the host user
chown -R "$(id -u):$(id -g)" /src/dist/rcalc /src/dist/.glibc-build.txt /src/target-appimage || true
'
}
build_binary_host() {
echo "Building release binary on the host ($(ldd --version | head -1))…"
(cd "${ROOT}" && cargo build --release)
install -D -m755 "${ROOT}/target/release/${PKG}" "${OUT_DIR}/${PKG}"
ldd --version | head -1 > "${OUT_DIR}/.glibc-build.txt" || true
}
ensure_binary() {
mkdir -p "${OUT_DIR}"
if [[ "${PREBUILT}" == "1" ]]; then
echo "PREBUILT=1: packaging ${OUT_DIR}/${PKG} as-is"
elif [[ "${SKIP_DOCKER}" == "1" ]]; then
build_binary_host
else
build_binary_docker
fi
if [[ ! -x "${OUT_DIR}/${PKG}" ]]; then
echo "error: ${OUT_DIR}/${PKG} missing — run without PREBUILT=1" >&2
exit 1
fi
}
# ---------------------------------------------------------------- staging ---
stage_tree() {
rm -rf "${STAGE}"
mkdir -p \
"${STAGE}/DEBIAN" \
"${STAGE}/usr/bin" \
"${STAGE}/usr/share/applications" \
"${STAGE}/usr/share/metainfo" \
"${STAGE}/usr/share/doc/${PKG}"
install -m755 "${OUT_DIR}/${PKG}" "${STAGE}/usr/bin/${PKG}"
if [[ "${NO_STRIP}" != "1" ]] && command -v strip >/dev/null 2>&1; then
strip --strip-unneeded "${STAGE}/usr/bin/${PKG}"
fi
install -m644 "${META_DIR}/${PKG}.desktop" \
"${STAGE}/usr/share/applications/${PKG}.desktop"
sed -e "s|@VERSION@|${VERSION}|" \
-e "s|@RELEASE_DATE@|$(date -u +%Y-%m-%d)|" \
"${META_DIR}/${PKG}.appdata.xml.in" \
> "${STAGE}/usr/share/metainfo/${PKG}.appdata.xml"
gzip -9 -c "${ROOT}/README.md" > "${STAGE}/usr/share/doc/${PKG}/README.md.gz"
echo "Binary: $(file -b "${STAGE}/usr/bin/${PKG}" | cut -d, -f1-3)"
}
# Icons for the desktop entry: /usr/share/icons/hicolor/<N>x<N>/apps/rcalc.png
#
# Source of truth, in order:
# 1. ico/*.png — ready-made sizes dropped in by hand. The REAL pixel size of
# each file decides which hicolor slot it fills (file names are ignored),
# and ico/ wins for the whole family: mixing ico/ artwork with assets/
# artwork would show a different picture depending on the icon size.
# 2. assets/icon.png resized to the standard sizes (needs ImageMagick).
# 3. the three PNGs bundled in assets/, as-is.
stage_icons() {
local dst="${STAGE}/usr/share/icons/hicolor"
local prepared=0 master="" master_size=0 size
local magick=""
for tool in magick convert; do
if command -v "${tool}" >/dev/null 2>&1; then
magick="${tool}"
break
fi
done
if [[ -d "${ROOT}/ico" ]] && [[ -n "${magick}" ]]; then
for f in "${ROOT}"/ico/*.png; do
[[ -f "${f}" ]] || continue
local w h
read -r w h <<<"$(identify -format '%w %h' "${f}" 2>/dev/null || echo '0 0')" || true
[[ "${w}" == "${h}" ]] && [[ "${w}" -gt 0 ]] || continue
if (( w > master_size )); then
master="${f}"
master_size="${w}"
fi
local slot="${dst}/${w}x${w}/apps/${PKG}.png"
if [[ ! -e "${slot}" ]]; then
mkdir -p "${dst}/${w}x${w}/apps"
install -m644 "${f}" "${slot}"
prepared=$((prepared + 1))
fi
done
fi
if (( prepared > 0 )); then
# Fill the remaining standard sizes from the biggest ico/ file — never
# upscale, so a 512px source does not produce a blurry 1024px icon.
if [[ -n "${magick}" ]]; then
for size in 16 24 32 48 64 96 128 256 512; do
[[ -e "${dst}/${size}x${size}/apps/${PKG}.png" ]] && continue
(( size <= master_size )) || continue
mkdir -p "${dst}/${size}x${size}/apps"
"${magick}" "${master}" -resize "${size}x${size}" \
"${dst}/${size}x${size}/apps/${PKG}.png"
done
fi
echo "Icons: ${prepared} ready-made from ico/ (master ${master_size}px)"
return 0
fi
if [[ -n "${magick}" ]]; then
for size in 16 24 32 48 64 128 256 512 1024; do
mkdir -p "${dst}/${size}x${size}/apps"
"${magick}" "${ROOT}/assets/icon.png" -resize "${size}x${size}" \
"${dst}/${size}x${size}/apps/${PKG}.png"
done
echo "Icons: resized from assets/icon.png (1024px master)"
else
install -D -m644 "${ROOT}/assets/icon-64.png" "${dst}/64x64/apps/${PKG}.png"
install -D -m644 "${ROOT}/assets/icon-256.png" "${dst}/256x256/apps/${PKG}.png"
install -D -m644 "${ROOT}/assets/icon.png" "${dst}/1024x1024/apps/${PKG}.png"
echo "Icons: assets/*.png as-is (ImageMagick not found)"
fi
}
# ------------------------------------------------------------ dependencies --
# Real library dependencies, read from the ELF itself. GL / X11 / Wayland
# libraries are dlopen()ed by winit at runtime, so they never show up in
# NEEDED — they are listed in Recommends in control.in instead.
compute_depends() {
local depends=""
if [[ "${USE_SHLIBDEPS:-1}" == "1" ]] && command -v dpkg-shlibdeps >/dev/null 2>&1; then
local tmp
tmp="$(mktemp -d)"
mkdir -p "${tmp}/debian"
# dpkg-shlibdeps insists on reading a debian/control next to it.
cat > "${tmp}/debian/control" <<EOF
Source: ${PKG}
Package: ${PKG}
Architecture: ${ARCH}
Depends: \${shlibs:Depends}
Description: temporary, for dpkg-shlibdeps
EOF
: > "${tmp}/debian/${PKG}.substvars"
(cd "${tmp}" && dpkg-shlibdeps -T"debian/${PKG}.substvars" "${STAGE}/usr/bin/${PKG}") \
>/dev/null 2>&1 || true
depends="$(sed -n 's/^shlibs:Depends=//p' "${tmp}/debian/${PKG}.substvars")"
rm -rf "${tmp}"
fi
if [[ -z "${depends}" ]]; then
echo "warning: dpkg-shlibdeps gave nothing, using a hand-written Depends list" >&2
depends='libc6, libfontconfig1, libgcc-s1'
fi
printf '%s' "${depends}"
}
# ---------------------------------------------------------------- control ---
stage_control() {
local installed_size depends
installed_size="$(du -s -k --exclude="${STAGE}/DEBIAN" "${STAGE}" | awk '{print $1}')"
depends="$(compute_depends)"
sed -e "s|@VERSION@|${DEB_VERSION}|" \
-e "s|@ARCHITECTURE@|${ARCH}|" \
-e "s|@MAINTAINER@|${MAINTAINER}|" \
-e "s|@INSTALLED_SIZE@|${installed_size}|" \
-e "s|@DEPENDS@|${depends}|" \
"${META_DIR}/control.in" > "${STAGE}/DEBIAN/control"
# Maintainer scripts must be executables owned by root in the archive.
install -m755 "${META_DIR}/postinst" "${STAGE}/DEBIAN/postinst"
install -m755 "${META_DIR}/postrm" "${STAGE}/DEBIAN/postrm"
(cd "${STAGE}" && find . -path ./DEBIAN -prune -o -type f -print \
| sed 's|^\./||' | sort | xargs -r md5sum) > "${STAGE}/DEBIAN/md5sums"
echo "Version: ${DEB_VERSION} (upstream ${VERSION} + revision ${DEB_REVISION})"
echo "Architecture: ${ARCH}"
echo "Maintainer: ${MAINTAINER}"
echo "Installed-Size: ${installed_size} KB"
echo "Depends: ${depends}"
}
# ------------------------------------------------------- version / update ---
# What "apt upgrade" needs: a version that is strictly greater than the one
# already installed, and a package name that does not change. Both are checked
# here so a release is never silently unpromotable.
check_version() {
case "${VERSION}" in
[0-9]*) ;;
*) echo "error: upstream version '${VERSION}' must start with a digit (dpkg rule)" >&2; exit 1 ;;
esac
if [[ ! "${VERSION}" =~ ^[0-9A-Za-z.+-~:]+$ || ! "${DEB_REVISION}" =~ ^[0-9A-Za-z.+-~]+$ ]]; then
echo "error: bad version '${DEB_VERSION}' — allowed chars are letters, digits and . + - ~ :" >&2
exit 1
fi
local installed
installed="$(dpkg-query -W -f='${Version}' "${PKG}" 2>/dev/null || true)"
if [[ -n "${installed}" ]]; then
if dpkg --compare-versions "${installed}" ge "${DEB_VERSION}"; then
echo "warning: ${PKG} ${installed} is ALREADY installed and is not older than ${DEB_VERSION}" >&2
echo " → 'apt upgrade' will do nothing. Bump Cargo.toml or DEB_REVISION." >&2
else
echo "Upgrade path: ${installed} → ${DEB_VERSION} (apt upgrade will pick this up)"
fi
else
echo "Upgrade path: fresh install of ${DEB_VERSION}"
fi
# scripts/install-desktop.sh links ~/.local/bin/rcalc → dist/rcalc, and
# ~/.local/bin comes before /usr/bin in PATH: the packaged binary would be
# shadowed by an old copy even after a successful upgrade.
if [[ -e "${HOME}/.local/bin/${PKG}" ]]; then
echo "warning: ${HOME}/.local/bin/${PKG} exists and shadows /usr/bin/${PKG}" >&2
echo " (installed by scripts/install-desktop.sh — remove it after switching to the .deb)" >&2
fi
}
# /usr/share/doc/rcalc/changelog.Debian.gz — Debian policy expects a changelog,
# and it is the human-readable history of what every version changed.
# packaging/deb/changelog (kept in git) wins; otherwise one entry is generated
# from the current commit so the package is never changelog-less.
stage_changelog() {
local doc="${STAGE}/usr/share/doc/${PKG}"
mkdir -p "${doc}"
local plain="${doc}/changelog.Debian" # gzip → changelog.Debian.gz (Debian-side history)
if [[ -f "${META_DIR}/changelog" ]]; then
cp "${META_DIR}/changelog" "${plain}"
echo "Changelog: packaging/deb/changelog"
else
local githash gitsubject
githash="$(git -C "${ROOT}" rev-parse --short HEAD 2>/dev/null || echo unknown)"
gitsubject="$(git -C "${ROOT}" log -1 --pretty=%s 2>/dev/null || echo packaging)"
{
printf '%s (%s) unstable; urgency=medium\n\n' "${PKG}" "${DEB_VERSION}"
printf ' * Upstream %s, packaged as %s.\n' "${VERSION}" "${DEB_VERSION}"
if [[ -f "${OUT_DIR}/.glibc-build.txt" ]]; then
printf ' * Built against %s\n' "$(cat "${OUT_DIR}/.glibc-build.txt")"
fi
printf ' * git %s: %s\n\n' "${githash}" "${gitsubject}"
printf ' -- %s %s\n' "${MAINTAINER}" "$(date -R)"
} > "${plain}"
echo "Changelog: generated (add packaging/deb/changelog to keep real history)"
fi
gzip -9n "${plain}" # -n: no timestamp → reproducible .gz
rm -f "${plain}"
}
# REPO=1 → turn dist/ into a flat apt repository, so updates can be installed
# with plain `apt update && apt upgrade` instead of pointing at a .deb file.
make_repo() {
[[ "${REPO:-0}" == "1" ]] || return 0
need_cmd dpkg-scanpackages
(
cd "${OUT_DIR}"
# One pass: Packages is the index, Packages.gz the compressed copy apt reads.
# Keep both files: apt reads Packages.gz, humans diff/read Packages.
dpkg-scanpackages --multiversion . /dev/null > Packages 2>/dev/null
gzip -9n -c Packages > Packages.gz
)
echo "Repo index: ${OUT_DIR}/Packages.gz ($(grep -c '^Package:' "${OUT_DIR}/Packages") entries)"
echo " apt one-liner: echo 'deb [trusted=yes] file:${OUT_DIR} ./' \\"
echo " | sudo tee /etc/apt/sources.list.d/${PKG}.list"
echo " then: sudo apt update && sudo apt install ${PKG}"
}
# ------------------------------------------------------------------ build ---
build_package() {
need_cmd dpkg-deb
rm -f "${DEB_FILE}"
# The stage tree inherits the build user's umask (often 002 → group-writable).
# Debian expects 755 dirs / 644 data files, plus the exec bits on the binary
# and the maintainer scripts.
find "${STAGE}" -type d -exec chmod 0755 {} +
find "${STAGE}" -type f -exec chmod 0644 {} +
chmod 0755 "${STAGE}/usr/bin/${PKG}" \
"${STAGE}/DEBIAN/postinst" "${STAGE}/DEBIAN/postrm"
# --root-owner-group: the stage tree is owned by the build user, the archive
# must not be (dpkg rejects foreign uids unless forced).
dpkg-deb --root-owner-group --build "${STAGE}" "${DEB_FILE}"
}
verify_package() {
local ok=1
# Fields straight from the archive (dpkg-deb --info output is localised).
dpkg-deb --field "${DEB_FILE}"
if command -v desktop-file-validate >/dev/null 2>&1; then
if desktop-file-validate "${STAGE}/usr/share/applications/${PKG}.desktop"; then
echo "desktop-file-validate: OK"
else
echo "warning: the desktop file did not validate" >&2
ok=0
fi
fi
# dist/rcalc can be stale: build.rs embeds APP_VERSION from Cargo.toml only at
# compile time, and the AppImage packaging had exactly that bug (old binary in
# a new image). Anything newer than the packaged binary means "rebuild it".
local stale
stale="$(find "${ROOT}/src" "${ROOT}/ui" "${ROOT}/build.rs" "${ROOT}/Cargo.toml" \
-newer "${OUT_DIR}/${PKG}" -print -quit 2>/dev/null || true)"
if [[ -n "${stale}" ]]; then
echo "warning: ${stale} is newer than the packaged binary" >&2
echo " (PREBUILT=1 / SKIP_DOCKER=1 reused an old dist/${PKG} — rebuild to be safe)" >&2
ok=0
fi
echo "Files in package: $(dpkg-deb -c "${DEB_FILE}" | grep -c '^-')"
if [[ "${KEEP_STAGE}" != "1" ]]; then
rm -rf "${STAGE}"
fi
echo
ls -l "${DEB_FILE}"
[[ "${ok}" == "1" ]] || echo "(check the warnings above)"
}
check_version
ensure_binary
stage_tree
stage_icons
stage_changelog
stage_control
build_package
make_repo
verify_package
echo
echo "OK: ${DEB_FILE}"
echo "Version: ${DEB_VERSION} (upstream ${VERSION} + revision ${DEB_REVISION})"
echo "Install: sudo apt install '${DEB_FILE}'"
echo "Next release: bump version in Cargo.toml (app changes) — or just"
echo " DEB_REVISION=2 ./scripts/build-deb.sh (packaging/icon only),"
echo " then install the new .deb: apt replaces the old one in place."
echo "Run: rcalc (or start it from the application menu)"
echo "Remove: sudo apt remove ${PKG}"