#!/bin/sh # Receipt Extractor installer for macOS (Apple Silicon). # # Install or update: curl -fsSL https://receipt-extractor-ca.pages.dev/install.sh | sh # Uninstall: curl -fsSL https://receipt-extractor-ca.pages.dev/install.sh | sh -s -- --uninstall # Also delete data: curl -fsSL https://receipt-extractor-ca.pages.dev/install.sh | sh -s -- --uninstall --delete-data # From a local file: sh install.sh --from receipt-extractor-0.1.0.tar.gz # # Everything goes in one folder, ~/Library/Application Support/Receipt Extractor: # app/ the program (replaced on update) # runtime/ uv, Python 3.12 and the pinned libraries (rebuilt on update) # data/ your documents and database (never touched by an update) # logs/ the app's log # plus a launcher, ~/Applications/Receipt Extractor.app, that starts the app and opens it in the # browser. Nothing else on the Mac is changed: no shell profile, no system folder, no admin password. # # The installer downloads the app, uv and Python. The installed app itself makes no network # connection except to Ollama on this Mac. set -eu APP_NAME="Receipt Extractor" RELEASE_URL="${PRX_RELEASE_URL:-https://receipt-extractor-ca.pages.dev/releases/receipt-extractor-latest.tar.gz}" UV_VERSION="0.12.18" # pinned; the app's own libraries are pinned in uv.lock PYTHON_VERSION="3.12" PORT="${PRX_PORT:-8765}" HOME_DIR="$HOME/Library/Application Support/$APP_NAME" APP_DIR="$HOME_DIR/app" RUNTIME="$HOME_DIR/runtime" DATA_DIR="$HOME_DIR/data" LAUNCHER="$HOME/Applications/$APP_NAME.app" case "${PRX_LANG:-$(defaults read -g AppleLocale 2>/dev/null || echo "${LANG:-en}")}" in fr*) FR=1 ;; *) FR=0 ;; esac say() { if [ "$FR" = 1 ]; then printf '%s\n' "$2"; else printf '%s\n' "$1"; fi; } fail() { printf '\n' >&2; say "Installation stopped: $1" "Installation interrompue : $2" >&2; exit 1; } stop_server() { pkill -f "$RUNTIME/venv/bin/python -m receipt_extractor.server" 2>/dev/null || true } # ---- options ---- FROM=""; UNINSTALL=0; DELETE_DATA=0 while [ $# -gt 0 ]; do case "$1" in --from) FROM="${2:?--from needs a file}"; shift 2 ;; --uninstall) UNINSTALL=1; shift ;; --delete-data) DELETE_DATA=1; shift ;; *) fail "unknown option $1" "option inconnue $1" ;; esac done if [ "$UNINSTALL" = 1 ]; then stop_server rm -rf "$APP_DIR" "$RUNTIME" "$HOME_DIR/logs" "$LAUNCHER" if [ "$DELETE_DATA" = 1 ]; then rm -rf "$HOME_DIR" say "$APP_NAME and all its data were removed." "$APP_NAME et toutes ses données ont été supprimés." else say "$APP_NAME was removed. Your documents are still in:" "$APP_NAME a été supprimé. Vos documents sont toujours dans :" say " $DATA_DIR" " $DATA_DIR" say "Delete that folder to remove them too." "Supprimez ce dossier pour les effacer aussi." fi exit 0 fi # ---- this Mac ---- [ "$(uname -s)" = Darwin ] || fail "$APP_NAME runs on macOS only." "$APP_NAME fonctionne seulement sous macOS." if [ "$(uname -m)" != arm64 ]; then fail "this Mac has an Intel processor. Reading receipts needs an Apple Silicon Mac (M1 or later)." \ "ce Mac a un processeur Intel. La lecture des reçus exige un Mac avec puce Apple (M1 ou plus récent)." fi MEM_GB=$(( $(sysctl -n hw.memsize) / 1073741824 )) if [ "$MEM_GB" -lt 16 ]; then say "Note: this Mac has $MEM_GB GB of memory. Reading works best with 16 GB or more; it may be slow." \ "Remarque : ce Mac a $MEM_GB Go de mémoire. La lecture fonctionne mieux avec 16 Go ou plus; elle pourrait être lente." fi say "Installing $APP_NAME in $HOME_DIR" "Installation de $APP_NAME dans $HOME_DIR" mkdir -p "$HOME_DIR" "$RUNTIME/bin" "$DATA_DIR" "$HOME_DIR/logs" # HTTPS only, except a local test server (sh install.sh with PRX_RELEASE_URL=http://localhost:...). case "$RELEASE_URL" in http://localhost:*|http://127.0.0.1:*) PROTO="--proto =http" ;; *) PROTO="--proto =https --tlsv1.2" ;; esac TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT # ---- 1. the program ---- say "1/3 Getting the program…" "1/3 Téléchargement du programme…" if [ -n "$FROM" ]; then cp "$FROM" "$TMP/release.tar.gz" || fail "can't read $FROM" "impossible de lire $FROM" else curl -fsSL $PROTO "$RELEASE_URL" -o "$TMP/release.tar.gz" \ || fail "the download failed. Check the internet connection and try again." \ "le téléchargement a échoué. Vérifiez la connexion Internet et réessayez." if curl -fsSL $PROTO "$RELEASE_URL.sha256" -o "$TMP/release.sha256" 2>/dev/null; then [ "$(shasum -a 256 "$TMP/release.tar.gz" | cut -d' ' -f1)" = "$(cut -d' ' -f1 < "$TMP/release.sha256")" ] \ || fail "the downloaded file is damaged (checksum mismatch). Try again." \ "le fichier téléchargé est endommagé (somme de contrôle invalide). Réessayez." fi fi mkdir "$TMP/new" tar -xzf "$TMP/release.tar.gz" -C "$TMP/new" --strip-components 1 \ || fail "the downloaded file is not a valid release." "le fichier téléchargé n'est pas une version valide." [ -f "$TMP/new/uv.lock" ] && [ -d "$TMP/new/src/receipt_extractor" ] \ || fail "the downloaded file is not a valid release." "le fichier téléchargé n'est pas une version valide." stop_server # an update: the running copy uses the old program rm -rf "$APP_DIR" mv "$TMP/new" "$APP_DIR" # ---- 2. Python and the pinned libraries, inside runtime/ ---- say "2/3 Setting up Python and the libraries (a few minutes the first time)…" \ "2/3 Préparation de Python et des bibliothèques (quelques minutes la première fois)…" UV="${PRX_UV:-$RUNTIME/bin/uv}" if ! "$UV" --version 2>/dev/null | grep -q "^uv $UV_VERSION" && [ -z "${PRX_UV:-}" ]; then curl -fsSL --proto '=https' --tlsv1.2 "https://astral.sh/uv/$UV_VERSION/install.sh" -o "$TMP/uv-install.sh" \ || fail "couldn't download uv. Check the internet connection and try again." \ "impossible de télécharger uv. Vérifiez la connexion Internet et réessayez." UV_UNMANAGED_INSTALL="$RUNTIME/bin" UV_NO_MODIFY_PATH=1 sh "$TMP/uv-install.sh" --quiet \ || fail "couldn't install uv." "impossible d'installer uv." fi export UV_NO_CONFIG=1 UV_PYTHON_PREFERENCE=only-managed export UV_PYTHON_INSTALL_DIR="$RUNTIME/python" UV_PROJECT_ENVIRONMENT="$RUNTIME/venv" export UV_CACHE_DIR="${UV_CACHE_DIR:-$RUNTIME/cache}" # --no-install-project: the app runs from app/src, so no build tool is fetched at install time. "$UV" sync --project "$APP_DIR" --frozen --no-dev --no-install-project --python "$PYTHON_VERSION" --quiet \ || fail "couldn't install the libraries. Check the internet connection and run the command again." \ "impossible d'installer les bibliothèques. Vérifiez la connexion Internet et relancez la commande." [ "$UV_CACHE_DIR" = "$RUNTIME/cache" ] && rm -rf "$RUNTIME/cache" # not needed once installed PYTHONPATH="$APP_DIR/src" "$RUNTIME/venv/bin/python" -c "import receipt_extractor.api.app" \ || fail "the program doesn't start. Run the command again; if it persists, contact support." \ "le programme ne démarre pas. Relancez la commande; si le problème persiste, communiquez avec le soutien." # ---- 3. the launcher ---- say "3/3 Adding $APP_NAME to your Applications…" "3/3 Ajout de $APP_NAME à vos Applications…" mkdir -p "$HOME/Applications" rm -rf "$LAUNCHER" mkdir -p "$LAUNCHER/Contents/MacOS" "$LAUNCHER/Contents/Resources" sed -e "s|@PORT@|$PORT|g" -e "s|@APP_NAME@|$APP_NAME|g" "$APP_DIR/installer/launcher.sh" > "$LAUNCHER/Contents/MacOS/launch" chmod +x "$LAUNCHER/Contents/MacOS/launch" VERSION=$(sed -n 's/^version = "\(.*\)"/\1/p' "$APP_DIR/pyproject.toml") sed -e "s|@APP_NAME@|$APP_NAME|g" -e "s|@VERSION@|$VERSION|g" "$APP_DIR/installer/Info.plist" > "$LAUNCHER/Contents/Info.plist" [ -f "$APP_DIR/installer/AppIcon.icns" ] && cp "$APP_DIR/installer/AppIcon.icns" "$LAUNCHER/Contents/Resources/" /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister \ -f "$LAUNCHER" >/dev/null 2>&1 || true printf '\n' say "$APP_NAME $VERSION is installed." "$APP_NAME $VERSION est installé." if [ ! -d /Applications/Ollama.app ] && [ ! -d "$HOME/Applications/Ollama.app" ] && ! command -v ollama >/dev/null 2>&1; then say "One more step: $APP_NAME reads receipts with Ollama, a free app. Install it from https://ollama.com/download" \ "Une dernière étape : $APP_NAME lit les reçus avec Ollama, une application gratuite. Installez-la depuis https://ollama.com/download" say "Then $APP_NAME will download the reading models for you." \ "$APP_NAME téléchargera ensuite les modèles de lecture pour vous." fi say "Open it any time from Applications ▸ $APP_NAME. Opening it now…" \ "Ouvrez-le en tout temps depuis Applications ▸ $APP_NAME. Ouverture…" [ -n "${PRX_NO_OPEN:-}" ] || open "$LAUNCHER"