Begin debfetcher

This commit is contained in:
Albert S. 2023-01-08 14:00:24 +01:00
commit 16da064a24
8 changed files with 458 additions and 0 deletions

91
README.md Normal file
View File

@ -0,0 +1,91 @@
# debfetcher
debfetcher automates fetching and installing certain .deb packages from apt repos.
## Features
- apt signature and package checksum verification
- User-level installation
## Motivation
Many popular software packages are often distributed as .deb packages, targeting Ubuntu primarily.
Often they are not available in the repos of others distros. Even if they are, sometimes the version tends to be behind upstream for some time. They may also only be available as "unofficial" packages through user repositories such as AUR, PPA or Gentoo overlays.
Examples for such packages are: Brave, Signal-Desktop, Element-Desktop, Spotify etc.
debfetcher can fetch such packages from the official apt repos.
The binaries in the .deb actually work on other distributions (often)
debfetcher simply identifies the latest version of a package in the corresponding apt repo, downloads it and then installs the binaries from the .deb.
debfetcher is intentionally dumb. It does not process post-install (or any other) scripts in the .deb, nor does it install any dependencies. It's not a package manager. What gets installed is controlled by the templates.
debfetcher verifies the repo signatures and .deb checksum (just like apt).
## Advantages
- You get the official build and don't have to rely on third-parties
- You don't have to wait for your distribution to make a version bump
## Status
It is, and will most certainly remain, a hack (although a useful one for me)
## FAQ
### Sounds overall rather dirty. Does this even work?
Naturally, this will not work for all packages due to ABI issues or library version mismatches.
However, using the .deb even for distros not based on debian is an approach taken by distris for propritary packages or packages where building from source is a significant maintenance load (i. e. electron-based apps). Overall, the idea is tested and not new.
### What if not all dependencies are installed?
debfetcher aims to be distro-agnostic. It will not install any dependencies.
If they are missing, you'll get an error (most likely when starting the app).
Hence, you will have to ensure yourself that they are installed. The packages debfetcher was tested on bundle some
of their dependencies. Also, in a typical Linux desktop installation chances are you already have all dependencies installed.
### Packages
Currently supported (= works for me) packages are in packages/.
### Templates
A template contains the repo URI, package name etc and specifies where to extract the binaries/libs contained in the .deb to. It may also install a .desktop file too.
## TODO
- Sandboxed download / extraction
- Rollback support
## Install
```
mkdir /var/lib/debfetcher
# It would not be unwise to verify those pubkeys
cp -R pubkeys /var/lib/debfetcher
# install defaults
cp -R packages /var/lib/debfetcher
```
## User-level installation
If you don't need/want a system-wide installation , debfetcher can be used to install a package in your local user profile. Therefore, debfetcher can be used without polluting your /.
Refer to `debfetcher.user.conf.sample` to change the appropriate settings.
Execute
```
DEBFETCHER_CONFIG="/path/to/debfetcher.user.conf" ./debfetcher.sh get signal
```
## Usage
### Install a package
```
debfetcher.sh get [packagename]
```
### Upgrade all
```
debfetcher.sh upgrade
```

7
debfetcher.conf.sample Normal file
View File

@ -0,0 +1,7 @@
TEMPLATES_PATH="/var/lib/debfetcher/templates"
PUBKEY_PATH="/var/lib/debfetcher/pubkeys/"
DB_PATH="$HOME/.local/share/debfetcher/"
CACHE_DIR="$HOME/.cache/debfetcher"
KEEP_OLD=0
DEBFETCHER_INSTALL_DESTDIR="$HOME/.debfetcher/"
DEBFETCHER_BIN_SYMLINK_DIR="$HOME/.debfetcher/bin"

265
debfetcher.sh Executable file
View File

@ -0,0 +1,265 @@
#!/bin/sh
# (C) 2023 - Albert S. <dev-debfetcher@quitesimple.org>
# Simple, dumb .deb fetcher (and installer) for non-debian distros
set -e
set -u
# can be overwritten by config file
PUBKEY_PATH="/var/lib/debfetcher/pubkeys/"
TEMPLATES_PATH="/var/lib/debfetcher/packages/"
DB_PATH="/var/db/debfetcher/"
CACHE_DIR="/tmp/debfetcher"
KEEP_OLD=0
DEBFETCHER_INSTALL_DESTDIR="/"
DEBFETCHER_UNPRIV_USER="debfetcher"
DEBFETCHER_BIN_SYMLINK_DIR="/usr/bin"
DEBFETCHER_CONFIG="${DEBFETCHER_CONFIG:-"/etc/debfetcher/conf"}"
fail()
{
echo $@ >&2
exit 1
}
unpriv()
{
if [ $(id -u) = "0" ] ; then
first="$1"
shift
su ${DEBFETCHER_UNPRIV_USER} -s $(which "$first") -- $@
else
$@
fi
}
ifroot()
{
CMD="$1"
shift
if [ $(id -u) = "0" ] ; then
$CMD $@
fi
}
check_debfetcher_dependencies()
{
curl --version &>/dev/null || fail "curl is missing or broken"
echo -e "1\n2" | sort -V &>/dev/null || fail "sort -V not available it seems"
gpg --version &>/dev/null || fail "gpg is missing or broken"
ar --version &>/dev/null || fail "ar is missing or broken"
tar --version &>/dev/null || fail "tar is missing or broken"
patchelf --version &>/dev/null || fail "patchelf is missing or broken"
}
get_higher_version()
{
echo -e "$1\n$2" | sort -V | tail -n 1
}
read_config()
{
if [ -f "${DEBFETCHER_CONFIG}" ] ; then
source "${DEBFETCHER_CONFIG}"
fi
}
print_usage()
{
echo "$0 get [package] - install/update the specified package"
echo "$0 upgrade - check each package for updates and install them"
}
verify_sig()
{
gpg --no-default-keyring --keyring "$1" --quiet --verify 2> "${CACHE_DIR}/last_gnupg_verify_result"
if [ $? -ne 0 ] ; then
fail "Signature check failed"
fi
}
debfetcher_install()
{
TEMPLATE_NAME=$(basename "$1")
template="${TEMPLATES_PATH}/${TEMPLATE_NAME}.debfetcher"
source $template
DEB_PATH="$2"
VERSION="$3"
echo "${TEMPLATE_NAME}: Installing: version ${VERSION}, file: ${DEB_PATH}"
TEMPDIR=$( mktemp -d -p "${CACHE_DIR}" )
ifroot chown "${DEBFETCHER_UNPRIV_USER}" "$TEMPDIR"
cd "$TEMPDIR"
mv "$DEB_PATH" .
unpriv ar x "$( basename "$DEB_PATH")"
mkdir data_contents
ifroot chown "${DEBFETCHER_UNPRIV_USER}" data_contents
datapkg="data.tar.xz"
[ -f "$datapkg" ] || datapkg="data.tar.gz"
[ -f "$datapkg" ] || datapkg="data.tar.bz2"
[ -f "$datapkg" ] || fail "no data archive found in .deb"
tar xf "$datapkg" -C data_contents
cd data_contents
#TODO: split doesn't have a benefit yet
pre_install || fail "Pre-install failed"
install || fail "Install failed"
post_install || fail "Post-install failed"
echo "$VERSION" > "/${DB_PATH}/${TEMPLATE_NAME}/version"
}
debfetcher_get()
{
TEMPLATE_NAME=$(basename "$1")
template="${TEMPLATES_PATH}/${TEMPLATE_NAME}.debfetcher"
[ -f "$template" ] || fail "Unknown package $1"
source $template
echo "${TEMPLATE_NAME}: Checking for new version..."
INRELEASE_URL="${APTURL}/dists/${DISTRO}/InRelease"
INRELEASE_CONTENT="$(unpriv curl -Ls $INRELEASE_URL)"
echo "${TEMPLATE_NAME}: Verifying apt repository PGP signature..."
echo "${INRELEASE_CONTENT}" | verify_sig "${PUBKEY}"
# Fetch
PACKAGES_SHA256SUM_SHOULD=$(echo "${INRELEASE_CONTENT}" | grep -E "${REPO}/binary-amd64/Packages$" | awk '{print $1}' | grep -E "^[0-9a-z]{64}$")
PACKAGES_URL="${APTURL}/dists/${DISTRO}/${REPO}/binary-amd64/Packages"
PACKAGES_CONTENT="$(unpriv curl -Ls "$PACKAGES_URL" && echo .)"
PACKAGES_CONTENT="${PACKAGES_CONTENT%.}"
PACKAGES_SHA256SUM_IS=$( echo -n "$PACKAGES_CONTENT" | sha256sum | awk '{print $1}' )
if [ "${PACKAGES_SHA256SUM_SHOULD}" != "${PACKAGES_SHA256SUM_IS}" ] ; then
fail "${TEMPLATE_NAME}: Packages checksum do not match for $1: ${PACKAGES_SHA256SUM_SHOULD} and ${PACKAGES_SHA256SUM_IS} "
fi
NORMALIZED=$(echo "${PACKAGES_CONTENT}" | grep -E "(Package:|Filename:|SHA256:|Version:)" | tr '\n' ' ' | sed -e 's/Package:/\nPackage:/g')
LATEST_VERSION=$( echo "${NORMALIZED}" | grep "Package: ${PACKAGE} " | sed -e 's/.*Version: //g' | awk '{print $1}' | sort -V | tail -n 1 )
CURRENT_VERSION="$( cat "${DB_PATH}/${TEMPLATE_NAME}/version" 2>/dev/null || true )"
if [ -z "${CURRENT_VERSION}" ] ; then
echo "${TEMPLATE_NAME}: First install of "$TEMPLATE_NAME", version $LATEST_VERSION"
else
if [ "$CURRENT_VERSION" != "$LATEST_VERSION" ] ; then
HIGHER=$(get_higher_version "$CURRENT_VERSION" "$LATEST_VERSION")
if [ "$HIGHER" != "$LATEST_VERSION" ] ; then
fail "Local version is newer than repo version"
fi
echo "${TEMPLATE_NAME}: Will upgrade "$TEMPLATE_NAME" from "$CURRENT_VERSION" to $LATEST_VERSION"
else
echo "${TEMPLATE_NAME}: Already up to date"
return
fi
fi
FILENAME=$( echo "${NORMALIZED}" | grep "Package: ${PACKAGE} " | grep "Version: $LATEST_VERSION" | sed -e 's/.*Filename: //g' | awk '{print $1}' )
FILENAME_BASENAME=$(basename "${FILENAME}")
DEB_URL="${APTURL}/${FILENAME}"
DEB_TARGET_PATH="${CACHE_DIR}/${FILENAME_BASENAME}"
touch "${DEB_TARGET_PATH}"
ifroot chown "${DEBFETCHER_UNPRIV_USER}" "${DEB_TARGET_PATH}"
echo "${TEMPLATE_NAME}: Fetching .deb..."
unpriv curl -Ls -o - "${DEB_URL}" > "${DEB_TARGET_PATH}" || fail "Fetch failure"
echo "${TEMPLATE_NAME}: Verifying checksums..."
DEB_HASH_MUST=$(echo "${NORMALIZED}" | grep "Package: ${PACKAGE} " | grep "Version: $LATEST_VERSION" | sed -e 's/.*SHA256: //g' | awk '{print $1}' )
DEB_HASH_IS=$(sha256sum "${DEB_TARGET_PATH}" | awk '{print $1}' )
if [ "${DEB_HASH_IS}" != "${DEB_HASH_MUST}" ] ; then
fail "${TEMPLATE_NAME}: .deb checksum mismatch for $TEMPLATE_NAME, file ${DEB_TARGET_PATH}: ${DEB_HASH_IS}, ${DEB_HASH_MUST}"
fi
debfetcher_install "${TEMPLATE_NAME}" "${DEB_TARGET_PATH}" "${LATEST_VERSION}"
}
init_db()
{
for template in ${TEMPLATES_PATH}/* ; do
basename=$(basename "$template" | sed -e 's/.debfetcher//g')
mkdir -p "${DB_PATH}/${basename}"
done
}
init_cache()
{
mkdir -p "${CACHE_DIR}"
ifroot chown root:root "${CACHE_DIR}"
ifroot chmod o=--- "${CACHE_DIR}"
rm -rf -- ${CACHE_DIR}/*
}
check_debfetcher_dependencies
read_config
init_db
init_cache
[ -w "${DEBFETCHER_INSTALL_DESTDIR}" ] || fail "No write access in install destdir"
mkdir -p "${DB_PATH}"
if [ $# -lt 1 ] ; then
print_usage
exit 1
fi
CMD="$1"
if [ "$CMD" = "get" ] ; then
if [ $# -lt 2 ] ; then
echo "$0 get [package]"
exit 1
fi
PACKAGE="$2"
debfetcher_get "${PACKAGE}"
fi
if [ "$CMD" = "upgrade" ] ; then
for template in ${TEMPLATES_PATH}/* ; do
template=$(basename "${template}" | sed -e 's/.debfetcher//g' )
if [ -f "${DB_PATH}/${template}/version" ] ; then
debfetcher_get ${template}
fi
done
fi
exit 0

View File

@ -0,0 +1,7 @@
TEMPLATES_PATH="$HOME/.local/share/debfetcher/packages"
PUBKEY_PATH="$HOME/.local/share/debfetcher/pubkeys"
DB_PATH="$HOME/.local/share/debfetcher/"
CACHE_DIR="$HOME/.cache/debfetcher"
KEEP_OLD=0
DEBFETCHER_INSTALL_DESTDIR="$HOME/.debfetcher/"
DEBFETCHER_BIN_SYMLINK_DIR="$HOME/.debfetcher/bin"

44
packages/brave.debfetcher Normal file
View File

@ -0,0 +1,44 @@
APTURL="https://brave-browser-apt-release.s3.brave.com"
DISTRO="stable"
REPO="main"
PUBKEY="${PUBKEY_PATH}/brave-browser-archive-keyring.gpg"
PACKAGE="brave-browser"
TS=$(date +%s)
THIS_BASEDIR="/opt/brave.com"
remove()
{
rm -rf -- "${DEBFETCHER_INSTALL_DESTDIR}/${THIS_BASEDIR}"
}
pre_install()
{
CURRENT_DIR="${DEBFETCHER_INSTALL_DESTDIR}${THIS_BASEDIR}"
if [ -d "${CURRENT_DIR}" ] ; then
mv -- "${DEBFETCHER_INSTALL_DESTDIR}${THIS_BASEDIR}" "${DEBFETCHER_INSTALL_DESTDIR}${THIS_BASEDIR}_${TS}"
fi
}
install()
{
cp -a --parents -- opt/brave.com "${DEBFETCHER_INSTALL_DESTDIR}"
cp --parents -- usr/share/applications/brave-browser.desktop "${DEBFETCHER_INSTALL_DESTDIR}"
chmod o=r "${DEBFETCHER_INSTALL_DESTDIR}"/usr/share/applications/brave-browser.desktop
}
post_install()
{
if [ ${KEEP_OLD} -eq 0 ] ; then
rm -rf -- "${DEBFETCHER_INSTALL_DESTDIR}/opt/brave.com_${TS}"
fi
sourcepath=$(realpath "${DEBFETCHER_INSTALL_DESTDIR}${THIS_BASEDIR}/brave/brave-browser")
ln -sf "${sourcepath}" "${DEBFETCHER_BIN_SYMLINK_DIR}/"
sed -e "s;Exec=/;Exec=${DEBFETCHER_BIN_SYMLINK_DIR};" -i "${DEBFETCHER_INSTALL_DESTDIR}"/usr/share/applications/brave-browser.desktop
}

View File

@ -0,0 +1,44 @@
APTURL="https://updates.signal.org/desktop/apt"
DISTRO="xenial"
REPO="main"
PUBKEY="${PUBKEY_PATH}/signal-desktop-keyring.gpg"
PACKAGE="signal-desktop"
TS=$(date +%s)
THIS_BASEDIR="/opt/Signal"
remove()
{
rm -rf -- "${DEBFETCHER_INSTALL_DESTDIR}/${THIS_BASEDIR}"
}
pre_install()
{
CURRENT_DIR="${DEBFETCHER_INSTALL_DESTDIR}${THIS_BASEDIR}"
if [ -d "${CURRENT_DIR}" ] ; then
mv -- "${DEBFETCHER_INSTALL_DESTDIR}${THIS_BASEDIR}" "${DEBFETCHER_INSTALL_DESTDIR}${THIS_BASEDIR}_${TS}"
fi
}
install()
{
#Inspired by Gentoo's ebuild
sed -e 's| --no-sandbox||g' -i usr/share/applications/signal-desktop.desktop
sed -e "s|/opt/Signal|${DEBFETCHER_INSTALL_DESTDIR}/opt/Signal|g" -i usr/share/applications/signal-desktop.desktop
cp -a --parents -- opt/Signal "${DEBFETCHER_INSTALL_DESTDIR}"
cp --parents -- usr/share/applications/signal-desktop.desktop "${DEBFETCHER_INSTALL_DESTDIR}"
chmod o=r ${DEBFETCHER_INSTALL_DESTDIR}/usr/share/applications/signal-desktop.desktop
ln -sf ${DEBFETCHER_INSTALL_DESTDIR}/opt/Signal/signal-desktop "${DEBFETCHER_BIN_SYMLINK_DIR}"
}
post_install()
{
if [ ${KEEP_OLD} -eq 0 ] ; then
rm -rf -- "${DEBFETCHER_INSTALL_DESTDIR}${THIS_BASEDIR}_${TS}"
fi
}

Binary file not shown.

Binary file not shown.