yams/scripts/build-arch-pkg.sh
trvon e389e384b1
feat(packaging): ship the daemon as a systemd user unit
The service is now user-bound: each user runs their own daemon with
their own config ($XDG_CONFIG_HOME/yams/config.toml), corpus
($XDG_DATA_HOME/yams) and socket ($XDG_RUNTIME_DIR/yams-daemon.sock),
which is exactly what the CLI already resolves. The system service, its
yams account/group, the group socket and the /etc/yams/config.toml
configuration go away.

- Unit: /usr/lib/systemd/user/yams-daemon.service runs
  `yams-daemon --foreground` with default path resolution,
  Restart=on-failure, WantedBy=default.target, and light hardening
  (NoNewPrivileges, LockPersonality, RestrictRealtime, RestrictSUIDSGID,
  native syscalls only). No ProtectHome/PrivateTmp: the daemon reads the
  files its user adds.
- Enablement: /usr/lib/systemd/user-preset/80-yams.preset plus
  `systemctl --global preset` on first install (and on upgrade from a
  release with the system unit), so the unit starts with every user's
  next session; administrators override with a user preset or
  `systemctl --global disable`, and later upgrades keep that choice.
  Package scripts never start per-user instances: they reload running
  user managers, try-restart running user daemons on upgrade and stop
  them on removal (`systemctl --user` via runuser per /run/user/<uid>),
  then `systemctl --global disable` on removal.
- Migration from 0.20.x and the system-service design: deb preinst, rpm
  %pre and Arch pre_upgrade stop and disable the old system unit while
  its file is still installed (deb also purges deb-systemd-helper
  state). /var/lib/yams, the old system corpus that the CLI never
  reached, is left alone and documented; user corpora are never
  touched.
- The sysusers entry, account creation and the deb's passwd and
  init-system-helpers dependencies are dropped; the deb purge no longer
  deletes /var/lib/yams.
- Install paths move to lib/systemd/user and lib/systemd/user-preset in
  meson, the Arch stage/PKGBUILD and the install-surface policy.
- docs/install.md: the user service, enabling it now
  (`systemctl --user enable --now`), lingering, plugins via the user's
  config, opting out, upgrade and removal behaviour, and the 0.20.3
  leftover.

Signed-off-by: trvon <me@trevon.dev>
2026-10-02 07:55:01 -06:00

435 lines
13 KiB
Bash
Executable file

#!/usr/bin/env bash
# Arch Linux package builder for YAMS.
#
# Builds yams inside an Arch Linux container (or native Arch host), produces
# a valid ALPM .pkg.tar.zst package, and optionally builds a pacman repository
# database with repo-add.
#
# Entry points:
# provision Install build dependencies (run once to pre-bake image)
# build Full build + package + repo (default)
# package_only <ver> <build_dir> <stage_dir> Package from pre-built stage
#
# Environment:
# RELEASE_CHANNEL stable, nightly, weekly (default: nightly)
# BUILD_DIR build output directory (default: build/release-pkg)
# MESON_EXTRA_ARGS extra meson configure flags
# CONAN_EXTRA_OPTS extra conan options
# GIT_REF tag/ref for version (default: auto from git describe)
#
# Artifacts:
# - build/<dir>/yams-<pkgver>-<pkgrel>-<arch>.pkg.tar.zst
# - build/<dir>/archrepo/os/<arch>/yams-<pkgver>-<pkgrel>-<arch>.pkg.tar.zst
# - build/<dir>/archrepo/os/<arch>/yams.db
# - build/<dir>/archrepo/os/<arch>/yams.files
#
# Example:
# docker run --rm \
# -v "$PWD:/workspace/yams" \
# -w /workspace \
# -e RELEASE_CHANNEL=nightly \
# yams/ci-arch-package:latest \
# bash -lc '/workspace/yams/scripts/build-arch-pkg.sh'
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
export PATH="${HOME}/.local/bin:/opt/venv/bin:${PATH}"
BUILD_DIR="${BUILD_DIR:-build/release-pkg}"
# Keep package builds separate from dev builds to avoid Conan cache collisions
PKG_BUILD_DIR="${BUILD_DIR}"
CHANNEL="${RELEASE_CHANNEL:-nightly}"
VERSION=""
HOST_UID="${HOST_UID:-}"
HOST_GID="${HOST_GID:-}"
log() { printf '\033[1;34m[arch-pkg]\033[0m %s\n' "$*"; }
fail() {
printf '\033[1;31m[arch-pkg]\033[0m %s\n' "$*" >&2
exit 1
}
usage() {
sed -n '2,28p' "${BASH_SOURCE[0]}"
}
# ── provisioning ──────────────────────────────────────────────────────────
provision_main() {
log "Provisioning Arch build toolchain"
if ! command -v pacman >/dev/null 2>&1; then
fail "pacman not available — this script expects an Arch Linux environment"
fi
pacman-key --init 2>/dev/null || true
pacman-key --populate archlinux 2>/dev/null || true
pacman -Syu --noconfirm --needed
pacman -S --noconfirm --needed \
base-devel \
clang \
cmake \
meson \
ninja \
python \
python-pip \
ccache \
git \
zip \
pkgconf \
liburing \
libarchive \
taglib \
sqlite \
openssl \
curl \
protobuf \
boost \
lld \
fakeroot
yes | pacman -Scc 2>/dev/null || true
# Conan via pip (not in Arch repos at a stable version)
if ! command -v conan >/dev/null 2>&1; then
python -m venv /opt/venv 2>/dev/null || true
/opt/venv/bin/pip install --no-cache-dir 'conan<3'
/opt/venv/bin/pip install --no-cache-dir --upgrade meson ninja
fi
log "Arch toolchain provisioned"
}
# ── helpers ───────────────────────────────────────────────────────────────
resolve_version() {
local v
if [ -n "${GIT_REF:-}" ]; then
v="${GIT_REF#refs/tags/}"
v="${v#yams-v}"
v="${v#v}"
elif [ -n "${VERSION}" ]; then
v="${VERSION}"
else
v="$(cd "${REPO_ROOT}" && git describe --tags --abbrev=7 2>/dev/null || echo "0.0.0")"
v="${v#yams-v}"
v="${v#v}"
fi
echo "${v}"
}
arch_label() {
case "$(uname -m)" in
x86_64) echo "x86_64" ;;
aarch64 | arm64) echo "aarch64" ;;
armv7l) echo "armv7h" ;;
*) uname -m ;;
esac
}
normalized_pkgver() {
printf '%s\n' "${1//-/_}"
}
package_release() {
printf '%s\n' "${YAMS_ARCH_PKGREL:-1}"
}
package_filename() {
local raw_version="$1"
local pkg_arch="$2"
printf 'yams-%s-%s-%s.pkg.tar.zst\n' \
"$(normalized_pkgver "${raw_version}")" \
"$(package_release)" \
"${pkg_arch}"
}
write_pkg_metadata() {
local package_root="$1"
local raw_version="$2"
local pkg_arch="$3"
local normalized_version pkgrel builddate install_size packager mtree_input
normalized_version="$(normalized_pkgver "${raw_version}")"
pkgrel="$(package_release)"
builddate="$(date -u +%s)"
install_size="$(du -sk "${package_root}" | awk '{print $1 * 1024}')"
packager="${PACKAGER:-YAMS Contributors <git@trevon.dev>}"
cat >"${package_root}/.PKGINFO" <<EOF
pkgname = yams
pkgbase = yams
pkgver = ${normalized_version}-${pkgrel}
pkgdesc = Yet Another Memory System — persistent content-addressed memory for LLMs
url = https://github.com/trvon/yams
builddate = ${builddate}
packager = ${packager}
size = ${install_size}
arch = ${pkg_arch}
license = GPL-3.0-or-later
depend = gcc-libs
depend = glibc
optdepend = onnxruntime: use the system ONNX Runtime instead of the bundled copy
EOF
cat >"${package_root}/.BUILDINFO" <<EOF
format = 2
pkgname = yams
pkgbase = yams
pkgver = ${normalized_version}-${pkgrel}
pkgarch = ${pkg_arch}
pkgbuild_sha256sum = SKIP
packager = ${packager}
builddate = ${builddate}
builddir = ${REPO_ROOT}
startdir = ${REPO_ROOT}
buildtool = custom-stage-packager
buildtoolver = 1
EOF
if [ -f "${REPO_ROOT}/packaging/arch/yams.install" ]; then
install -Dm644 "${REPO_ROOT}/packaging/arch/yams.install" "${package_root}/.INSTALL"
fi
if command -v bsdtar >/dev/null 2>&1; then
mtree_input=".MTREE.input"
(
cd "${package_root}"
LC_ALL=C find . -mindepth 1 ! -name '.MTREE' -print0 >"${mtree_input}"
xargs -0 bsdtar -czf .MTREE --format=mtree \
--options='!all,use-set,type,uid,gid,mode,time,size,md5,sha256,link' \
<"${mtree_input}"
rm -f "${mtree_input}"
)
fi
}
build_arch_package_from_stage() {
local stage_root="$1"
local work_dir="$2"
local raw_version="$3"
local pkg_arch="$4"
local package_root pkg_file package_path
mkdir -p "${work_dir}"
work_dir="$(cd "${work_dir}" && pwd)"
package_root="${work_dir}/package-root"
pkg_file="$(package_filename "${raw_version}" "${pkg_arch}")"
package_path="${work_dir}/${pkg_file}"
rm -rf "${package_root}" "${package_path}"
mkdir -p "${package_root}"
cp -a "${stage_root}/." "${package_root}/"
write_pkg_metadata "${package_root}" "${raw_version}" "${pkg_arch}"
(
cd "${package_root}"
shopt -s dotglob nullglob
package_entries=(*)
COPYFILE_DISABLE=1 bsdtar --no-mac-metadata -cf - "${package_entries[@]}" |
zstd -T0 -19 -o "${package_path}"
)
printf '%s\n' "${package_path}"
}
# ── build ─────────────────────────────────────────────────────────────────
build_main() {
VERSION="$(resolve_version)"
local arch pkg_arch meson_build_dir stage_root work_dir package_path pkg_file
arch="$(arch_label)"
pkg_arch="${arch}"
meson_build_dir="${YAMS_BUILD_DIR:-${PKG_BUILD_DIR}/meson-${pkg_arch}}"
log "Building YAMS ${VERSION} for arch ${arch} (channel=${CHANNEL})"
log "Using Arch Meson build dir: ${meson_build_dir}"
cd "${REPO_ROOT}"
# Configure and build with Meson + Conan (same flow as Linux CI)
export YAMS_CONAN_HOST_PROFILE="./conan/profiles/host-linux-clang"
export YAMS_CONAN_ARCH="${arch}"
export YAMS_BUILD_DIR="${meson_build_dir}"
export YAMS_ENABLE_MOBILE_BINDINGS=false
export YAMS_EXTRA_MESON_FLAGS="-Dyams-version=${VERSION} -Dwerror=false -Dwarning_level=2 ${MESON_EXTRA_ARGS:-}"
export YAMS_INSTALL_PREFIX="/usr"
export CC=clang
export CXX=clang++
# ONNX Runtime is opt-in in setup.sh; the Arch packages keep shipping the ONNX plugin
# (and ONNX-backed Glint) like the other release targets.
bash ./setup.sh Release --with-onnx
# Source Conan build env
local conan_env=""
for candidate in \
"${YAMS_BUILD_DIR}/build-release/conan/conanbuild.sh" \
"${YAMS_BUILD_DIR}/conan/conanbuild.sh"; do
if [ -f "$candidate" ]; then
conan_env="$candidate"
break
fi
done
if [ -n "$conan_env" ]; then
set +u
# shellcheck source=/dev/null
source "$conan_env"
set -u
fi
meson compile -C "${YAMS_BUILD_DIR}"
# Stage install. Meson interprets a relative --destdir from the build directory,
# so keep this absolute; otherwise the package step can archive an empty
# repo-root stage while the real payload lands under ${YAMS_BUILD_DIR}/...
case "${PKG_BUILD_DIR}" in
/*) stage_root="${PKG_BUILD_DIR}/stage" ;;
*) stage_root="${REPO_ROOT}/${PKG_BUILD_DIR}/stage" ;;
esac
rm -rf "${stage_root}"
meson install -C "${YAMS_BUILD_DIR}" --destdir "${stage_root}" --no-rebuild
# Strip the staged binaries (the install rules already stage only the runtime set)
bash scripts/split-debug-symbols.sh "${stage_root}/usr"
# Fail the package rather than silently shipping without the embedding provider.
if ! compgen -G "${stage_root}/usr/lib/yams/plugins/libyams_onnx_plugin.so*" >/dev/null; then
echo "error: ONNX plugin missing from Arch package stage (${stage_root}/usr/lib/yams/plugins)" >&2
exit 1
fi
# Install the systemd user unit + user preset into stage.
install -Dm644 packaging/systemd/yams-daemon.service \
"${stage_root}/usr/lib/systemd/user/yams-daemon.service"
install -Dm644 packaging/systemd/80-yams.preset \
"${stage_root}/usr/lib/systemd/user-preset/80-yams.preset"
log "Assembling Arch package from staged install"
work_dir="${PKG_BUILD_DIR}/arch-work"
rm -rf "${work_dir}"
mkdir -p "${work_dir}"
package_path="$(build_arch_package_from_stage "${stage_root}" "${work_dir}" "${VERSION}" "${pkg_arch}")"
pkg_file="$(basename "${package_path}")"
mkdir -p "${REPO_ROOT}/${PKG_BUILD_DIR}"
cp "${package_path}" "${REPO_ROOT}/${PKG_BUILD_DIR}/"
cd "${REPO_ROOT}"
# Chown artifacts back to host user if running in Docker
if [ -n "${HOST_UID:-}" ] && [ -n "${HOST_GID:-}" ] && [ "${HOST_UID}" != "0" ]; then
chown -R "${HOST_UID}:${HOST_GID}" "${PKG_BUILD_DIR}" 2>/dev/null || true
fi
log "Created Arch package: ${PKG_BUILD_DIR}/${pkg_file}"
# ── repo metadata ────────────────────────────────────────────────────
build_repo_metadata "${pkg_arch}"
echo "ARCH_PKG=${PKG_BUILD_DIR}/${pkg_file}" >>"${GITHUB_ENV:-/dev/null}"
}
# ── package_only ──────────────────────────────────────────────────────────
package_only() {
VERSION="${1:-}"
local build_dir="${2:-}"
local stage_dir="${3:-}"
local pkg_arch work_dir package_path pkg_file
if [ -z "${VERSION}" ] || [ -z "${build_dir}" ] || [ -z "${stage_dir}" ]; then
fail "usage: package_only <version> <build_dir> <stage_dir>"
fi
pkg_arch="$(arch_label)"
PKG_BUILD_DIR="${build_dir}"
work_dir="${build_dir}/.arch-package-work"
mkdir -p "${build_dir}"
log "Packaging staged install at ${stage_dir} -> ${build_dir}"
package_path="$(build_arch_package_from_stage "${stage_dir}" "${work_dir}" "${VERSION}" "${pkg_arch}")"
pkg_file="$(basename "${package_path}")"
cp "${package_path}" "${build_dir}/${pkg_file}"
rm -rf "${work_dir}"
log "Created Arch package: ${build_dir}/${pkg_file}"
build_repo_metadata "${pkg_arch}"
}
# ── repo metadata ─────────────────────────────────────────────────────────
build_repo_metadata() {
local pkg_arch="${1:-$(arch_label)}"
local repo_dir="${REPO_ROOT}/${PKG_BUILD_DIR}/archrepo"
local arch_dir="${repo_dir}/os/${pkg_arch}"
local usage_file
mkdir -p "${arch_dir}"
# Collect all matching .pkg.tar.zst files into the standard Arch repo layout.
find "${REPO_ROOT}/${PKG_BUILD_DIR}" -maxdepth 1 -name "yams-*-${pkg_arch}.pkg.tar.zst" \
-exec cp -f {} "${arch_dir}/" \;
# Build pacman database with repo-add in the same directory pacman will fetch from.
if command -v repo-add >/dev/null 2>&1; then
(
cd "${arch_dir}"
shopt -s nullglob
packages=(*.pkg.tar.zst)
if [ ${#packages[@]} -eq 0 ]; then
log "No Arch packages found under ${arch_dir}; skipping repo-add"
exit 0
fi
rm -f yams.db yams.db.tar.gz yams.files yams.files.tar.gz
repo-add -q yams.db.tar.gz "${packages[@]}"
ln -sf yams.db.tar.gz yams.db
ln -sf yams.files.tar.gz yams.files
)
else
log "repo-add not available; skipping pacman database generation"
fi
usage_file="${repo_dir}/USAGE.txt"
cat >"${usage_file}" <<'EOF'
Add Arch repo:
sudo tee /etc/pacman.d/yams.conf <<'REPO'
[yams]
SigLevel = Optional TrustAll
Server = https://repo.yamsmemory.ai/archrepo/os/$arch
REPO
sudo tee -a /etc/pacman.conf <<'REPO'
Include = /etc/pacman.d/yams.conf
REPO
sudo pacman -Sy yams
EOF
log "Arch repo metadata generated at ${arch_dir}"
}
# ── dispatch ──────────────────────────────────────────────────────────────
case "${1:-build}" in
provision)
provision_main
;;
build)
build_main
;;
package_only)
shift
package_only "$@"
;;
-h | --help)
usage
exit 0
;;
*)
usage
fail "unknown command: ${1:-}"
;;
esac