utils/freedays/install.sh
Krzysztof kuhy Rudnicki aa032c95e4 Drop the gatelock dependency, and actually sync the pool
Two things the pool needed to be real rather than merely correct.

**freedays no longer depends on gatelock.** It imported
gatelock.log_integrity for ~15 lines of HMAC, but importing gatelock runs
its __init__, which imports tkinter for the lock-window machinery. So a
headless policy library had acquired a hard dependency on a GUI toolkit,
and `import freedays` failed anywhere without Tk:

    ImportError: libtk8.6.so: cannot open shared object file

A vmbox run caught it. The host masked it completely -- Tk is installed
here for the lockers -- and so did CI, whose own comment claimed "no
display needed" while the job would have needed libtk. freedays/_hmac.py
now does the signing with hashlib and hmac.

The format is deliberately unchanged (HMAC-SHA256 over compact
sorted-key JSON), so nothing has to be re-signed: all 8 entries already
in the real audit trail, written by gatelock's implementation, verify
against the new one. That was checked, not assumed.

This also dissolves the version diamond that shaped the app rollout:
freedays now pins only crdt-sync, so it can never conflict with whatever
gatelock tag an app carries.

**The pool now syncs on a schedule.** A `sync-quiet` subcommand plus a
systemd user timer every 15 minutes, because "it syncs" was otherwise
only true if you typed `freedays sync` yourself -- a day marked on the
phone would sit there. sync-quiet exits 0 when there is no network or no
credential, on purpose: a unit that goes red every time the laptop is
offline is a unit you learn to ignore, and a real bug still reaches the
journal through the library's logging.

install.sh gives freedays its own venv rather than borrowing an app's --
the pool is shared by five apps, so binding its CLI to one of them would
make "is freedays installed?" depend on something unrelated, and
uninstalling that app would take the command with it.

Verified in vmbox first (systemd), then on this host: the timer is
enabled and scheduled, and the service *finishes* rather than failing
when there is no remote.

seed_session.py gains freedays, without which the one command that can
give the pool a Firebase session would reject it outright (--app is
choices-validated) and the pool would stay local-only with nothing
saying why. That file was already exactly at the 250-line cap, so the
app list and its rationale move to tool/_seeded_apps.py.

freedays: 109 tests, 100% branch coverage. crdt-sync: 433 pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 15:56:42 +02:00

117 lines
3.9 KiB
Bash
Executable File

#!/bin/bash
# ============================================================================
# Install the freedays CLI and its sync timer for the current user.
#
# freedays gets its OWN venv rather than borrowing an app's. The pool is
# shared by five apps, so binding the command that manages it to whichever
# app venv happened to be active would make "is freedays installed?" depend
# on something unrelated -- and uninstalling that app would take the CLI with
# it. This is idempotent: rerun it after any change.
# ============================================================================
set -euo pipefail
SCRIPT_NAME="$(basename "$0")"
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
UTILS_DIR="$(dirname "$REPO_DIR")"
readonly SCRIPT_NAME REPO_DIR UTILS_DIR
readonly STATE_DIR="${HOME}/.local/share/freedays"
readonly VENV_DIR="${STATE_DIR}/venv"
readonly BIN_DIR="${HOME}/.local/bin"
readonly UNIT_DIR="${HOME}/.config/systemd/user"
readonly SYSTEM_PYTHON="/usr/bin/python3"
log() { printf 'install: %s\n' "$1" >&2; }
fail() { printf 'install: FAILED -- %s\n' "$1" >&2; exit 1; }
usage() {
echo "Usage: $SCRIPT_NAME [--no-timer]"
echo " --no-timer install the CLI only; do not enable the sync timer"
exit 0
}
INSTALL_TIMER=1
build_venv() {
log "building the freedays venv at $VENV_DIR"
mkdir -p "$STATE_DIR"
if [[ ! -x "$VENV_DIR/bin/python" ]]; then
"$SYSTEM_PYTHON" -m venv "$VENV_DIR" || fail "could not create the venv"
fi
# Siblings install from the working tree, not from a tag: this script runs
# from a checkout, and installing the released tag here would silently
# test something other than what is on disk.
"$VENV_DIR/bin/pip" install --quiet --upgrade pip || fail "pip self-upgrade"
"$VENV_DIR/bin/pip" install --quiet "$UTILS_DIR/crdt-sync" \
|| fail "the crdt-sync sibling package"
"$VENV_DIR/bin/pip" install --quiet --no-deps -e "$REPO_DIR" \
|| fail "freedays itself"
}
link_cli() {
log "putting the freedays command on PATH at $BIN_DIR/freedays"
mkdir -p "$BIN_DIR"
ln -sf "$VENV_DIR/bin/freedays" "$BIN_DIR/freedays"
}
install_timer() {
log "installing the sync timer"
mkdir -p "$UNIT_DIR"
install -m 644 "$REPO_DIR/systemd/freedays-sync.service" "$UNIT_DIR/"
install -m 644 "$REPO_DIR/systemd/freedays-sync.timer" "$UNIT_DIR/"
systemctl --user daemon-reload || fail "systemctl daemon-reload"
systemctl --user enable --now freedays-sync.timer || fail "enabling the timer"
}
verify() {
# The check that matters: the command resolves, and the gate apps'
# interpreter can import the library. Those are two different
# environments and either can be broken on its own.
log "verifying"
"$BIN_DIR/freedays" status >/dev/null || fail "the freedays CLI does not run"
"$SYSTEM_PYTHON" -c "import freedays" 2>/dev/null || {
log "NOTE: the system python cannot import freedays yet."
log " The gate apps need it there; their own install.sh does that."
}
if [[ "$INSTALL_TIMER" -eq 1 ]]; then
systemctl --user is-enabled freedays-sync.timer >/dev/null \
|| fail "the sync timer is not enabled"
fi
}
main() {
build_venv
link_cli
if [[ "$INSTALL_TIMER" -eq 1 ]]; then
install_timer
fi
verify
echo "============================================================"
"$BIN_DIR/freedays" status
echo "============================================================"
log "done. 'freedays mark today' takes the day off, everywhere."
if [[ ":$PATH:" != *":$BIN_DIR:"* ]]; then
log "NOTE: $BIN_DIR is not on your PATH; add it to use 'freedays' bare."
fi
}
while [[ $# -gt 0 ]]; do
case $1 in
--no-timer)
INSTALL_TIMER=0
shift
;;
-h|--help)
usage
;;
*)
echo "Unknown option: $1" >&2
exit 1
;;
esac
done
main "$@"