From 300ef54a91dd541c83073777edddae07443da676 Mon Sep 17 00:00:00 2001 From: s0k0l Date: Sat, 10 Oct 2026 12:24:13 +0300 Subject: [PATCH] chore: init --- .gitignore | 2 + README.md | 3 + install-app | 310 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 315 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 install-app diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a59d75 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +.venv/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..414d955 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Adopt + +Приложение для установки бинарных файлов и программных пакетов без инсталлятора. Интегрирует бинарник в ~/.local и создает для него Desktop файл. \ No newline at end of file diff --git a/install-app b/install-app new file mode 100755 index 0000000..0e130a3 --- /dev/null +++ b/install-app @@ -0,0 +1,310 @@ +#!/usr/bin/env bash +# install-app — install executables (AppImage/ELF/script) to PATH with .desktop integration +# Usage: install-app [OPTIONS] + +set -euo pipefail + +# ── Colors ──────────────────────────────────────────────────────────────────── +RED='\033[0;31m' +GRN='\033[0;32m' +YLW='\033[1;33m' +BLU='\033[0;34m' +BLD='\033[1m' +RST='\033[0m' + +info() { echo -e "${BLU}::${RST} $*"; } +success() { echo -e "${GRN}✓${RST} $*"; } +warn() { echo -e "${YLW}⚠${RST} $*"; } +error() { echo -e "${RED}✗${RST} $*" >&2; } +die() { error "$*"; exit 1; } + +# ── Defaults ────────────────────────────────────────────────────────────────── +GLOBAL=false +ICON="" +APP_NAME="" +CATEGORY="Utility" +UPDATE_FILE="" +FORCE=false +UNINSTALL=false + +# ── Help ────────────────────────────────────────────────────────────────────── +usage() { +cat < + +${BLD}OPTIONS${RST} + -g, --global Install system-wide (/usr/local/bin, /usr/share/) + Requires sudo / root + -i, --icon Icon file (png/svg/xpm) + -n, --name Display name (default: derived from filename) + -c, --category .desktop category (default: Utility) + --update Replace binary with new version (keeps .desktop & icon) + --uninstall Remove binary, icon and .desktop entry + --force Overwrite/remove without confirmation + -h, --help Show this help + +${BLD}EXAMPLES${RST} + install-app ~/Downloads/MyApp.AppImage + install-app -g -i ~/icon.png -n "My App" ~/Downloads/MyApp.AppImage + install-app --update ~/Downloads/MyApp-v2.AppImage -n "My App" + install-app --uninstall -n "My App" + sudo install-app -g --uninstall -n "My App" +EOF +} + +# ── Argument parsing ────────────────────────────────────────────────────────── +POSITIONAL=() + +while [[ $# -gt 0 ]]; do + case "$1" in + -g|--global) GLOBAL=true; shift ;; + -i|--icon) ICON="$2"; shift 2 ;; + -n|--name) APP_NAME="$2"; shift 2 ;; + -c|--category) CATEGORY="$2"; shift 2 ;; + --update) UPDATE_FILE="$2"; shift 2 ;; + --uninstall) UNINSTALL=true; shift ;; + --force) FORCE=true; shift ;; + -h|--help) usage; exit 0 ;; + -*) die "Unknown option: $1. Use -h for help." ;; + *) POSITIONAL+=("$1"); shift ;; + esac +done + +# ── Resolve executable ──────────────────────────────────────────────────────── +if $UNINSTALL; then + # uninstall: -n required OR positional arg for name derivation + if [[ -z "$APP_NAME" ]]; then + if [[ ${#POSITIONAL[@]} -gt 0 ]]; then + SRC_FILE="$(realpath "${POSITIONAL[0]}" 2>/dev/null || echo "${POSITIONAL[0]}")" + else + die "Provide app name via -n or pass the original executable as argument." + fi + else + SRC_FILE="" + fi +elif [[ -n "$UPDATE_FILE" ]]; then + SRC_FILE="$UPDATE_FILE" + SRC_FILE="$(realpath "$SRC_FILE")" + [[ -f "$SRC_FILE" ]] || die "File not found: $SRC_FILE" +else + [[ ${#POSITIONAL[@]} -eq 0 ]] && { usage; exit 1; } + SRC_FILE="${POSITIONAL[0]}" + SRC_FILE="$(realpath "$SRC_FILE")" + [[ -f "$SRC_FILE" ]] || die "File not found: $SRC_FILE" +fi + +# ── Derive app name ─────────────────────────────────────────────────────────── +if [[ -z "$APP_NAME" ]]; then + BASENAME="$(basename "${SRC_FILE:-}")" + # Strip known extensions: .AppImage, .appimage, .sh, .py, .bin, .elf + APP_NAME="${BASENAME%%.AppImage}" + APP_NAME="${APP_NAME%%.appimage}" + APP_NAME="${APP_NAME%%.sh}" + APP_NAME="${APP_NAME%%.py}" + APP_NAME="${APP_NAME%%.bin}" + APP_NAME="${APP_NAME%%.elf}" + # Strip version suffixes like -x86_64, -1.2.3, _amd64 + APP_NAME="$(echo "$APP_NAME" | sed -E 's/[-_][vV]?[0-9]+[._0-9]*[-_]?(x86_64|amd64|arm64|aarch64)?$//')" + APP_NAME="$(echo "$APP_NAME" | sed -E 's/[-_](x86_64|amd64|arm64|aarch64)$//')" +fi + +DISPLAY_NAME="$APP_NAME" +BIN_NAME="$(echo "$APP_NAME" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')" +DESKTOP_NAME="$(echo "$BIN_NAME" | tr '[:upper:]' '[:lower:]')" + +# ── Paths ───────────────────────────────────────────────────────────────────── +if $GLOBAL; then + BIN_DIR="/usr/local/bin" + ICON_DIR="/usr/share/icons/hicolor/256x256/apps" + DESKTOP_DIR="/usr/share/applications" + DESKTOP_DB_DIR="/usr/share/applications" +else + BIN_DIR="$HOME/.local/bin" + ICON_DIR="$HOME/.local/share/icons/hicolor/256x256/apps" + DESKTOP_DIR="$HOME/.local/share/applications" + DESKTOP_DB_DIR="$HOME/.local/share/applications" +fi + +DEST_BIN="$BIN_DIR/$BIN_NAME" +DESKTOP_FILE="$DESKTOP_DIR/${DESKTOP_NAME}.desktop" + +# ── Sudo check ──────────────────────────────────────────────────────────────── +if $GLOBAL && [[ $EUID -ne 0 ]]; then + die "System-wide install requires root. Run: sudo install-app -g $*" +fi + +# ── UNINSTALL MODE ──────────────────────────────────────────────────────────── +if $UNINSTALL; then + info "Uninstalling ${BLD}$DISPLAY_NAME${RST}" + + FOUND=false + + # Check both scopes + USER_BIN="$HOME/.local/bin/$BIN_NAME" + SYS_BIN="/usr/local/bin/$BIN_NAME" + + if $GLOBAL; then + BINS_TO_CHECK=("$SYS_BIN") + ICON_DIRS_TO_CHECK=("/usr/share/icons/hicolor") + DESKTOP_FILES_TO_CHECK=("/usr/share/applications/${DESKTOP_NAME}.desktop") + DB_DIR="/usr/share/applications" + else + BINS_TO_CHECK=("$USER_BIN") + ICON_DIRS_TO_CHECK=("$HOME/.local/share/icons/hicolor") + DESKTOP_FILES_TO_CHECK=("$HOME/.local/share/applications/${DESKTOP_NAME}.desktop") + DB_DIR="$HOME/.local/share/applications" + fi + + # Sudo check for global + if $GLOBAL && [[ $EUID -ne 0 ]]; then + die "System-wide uninstall requires root. Run: sudo install-app -g --uninstall -n \"$DISPLAY_NAME\"" + fi + + if ! $FORCE; then + warn "About to remove:" + for b in "${BINS_TO_CHECK[@]}"; do [[ -f "$b" ]] && echo " $b"; done + for d in "${DESKTOP_FILES_TO_CHECK[@]}"; do [[ -f "$d" ]] && echo " $d"; done + for idir in "${ICON_DIRS_TO_CHECK[@]}"; do + find "$idir" -name "${DESKTOP_NAME}.*" 2>/dev/null | while read -r f; do echo " $f"; done + done + read -rp " Continue? [y/N] " yn + [[ "${yn,,}" == "y" ]] || die "Aborted." + fi + + # Remove binary + for b in "${BINS_TO_CHECK[@]}"; do + if [[ -f "$b" ]]; then + rm -f "$b" + success "Removed binary: $b" + FOUND=true + fi + done + + # Remove .desktop + for d in "${DESKTOP_FILES_TO_CHECK[@]}"; do + if [[ -f "$d" ]]; then + rm -f "$d" + success "Removed .desktop: $d" + FOUND=true + fi + done + + # Remove icon(s) + for idir in "${ICON_DIRS_TO_CHECK[@]}"; do + while IFS= read -r iconfile; do + rm -f "$iconfile" + success "Removed icon: $iconfile" + FOUND=true + done < <(find "$idir" -name "${DESKTOP_NAME}.*" 2>/dev/null) + done + + # Update desktop db + if command -v update-desktop-database &>/dev/null; then + update-desktop-database "$DB_DIR" 2>/dev/null || true + fi + + $FOUND || warn "Nothing found to remove for '$BIN_NAME'" + exit 0 +fi + +# ── UPDATE MODE ─────────────────────────────────────────────────────────────── +if [[ -n "$UPDATE_FILE" ]]; then + info "Update mode for: ${BLD}$DISPLAY_NAME${RST}" + + # Find installed binary + if [[ ! -f "$DEST_BIN" ]]; then + # Try the other scope + ALT_BIN="" + if $GLOBAL; then + ALT_BIN="$HOME/.local/bin/$BIN_NAME" + else + ALT_BIN="/usr/local/bin/$BIN_NAME" + fi + if [[ -f "$ALT_BIN" ]]; then + DEST_BIN="$ALT_BIN" + warn "Found binary in alternate location: $DEST_BIN" + else + die "No installed binary found for '$BIN_NAME'. Install it first." + fi + fi + + info "Replacing: $DEST_BIN" + cp "$SRC_FILE" "$DEST_BIN" + chmod +x "$DEST_BIN" + success "Binary updated: $DEST_BIN" + exit 0 +fi + +# ── INSTALL MODE ────────────────────────────────────────────────────────────── +info "Installing ${BLD}$DISPLAY_NAME${RST} $(if $GLOBAL; then echo '(system-wide)'; else echo "(user: $USER)"; fi)" + +# Confirm overwrite +if [[ -f "$DEST_BIN" ]] && ! $FORCE; then + warn "Binary already exists: $DEST_BIN" + read -rp " Overwrite? [y/N] " yn + [[ "${yn,,}" == "y" ]] || die "Aborted." +fi + +# Create directories +mkdir -p "$BIN_DIR" "$ICON_DIR" "$DESKTOP_DIR" + +# ── Copy binary ─────────────────────────────────────────────────────────────── +cp "$SRC_FILE" "$DEST_BIN" +chmod +x "$DEST_BIN" +success "Binary installed: $DEST_BIN" + +# ── Handle icon ─────────────────────────────────────────────────────────────── +ICON_VALUE="application-x-executable" + +if [[ -n "$ICON" ]]; then + ICON="$(realpath "$ICON")" + [[ -f "$ICON" ]] || die "Icon file not found: $ICON" + ICON_EXT="${ICON##*.}" + DEST_ICON="$ICON_DIR/${DESKTOP_NAME}.${ICON_EXT}" + cp "$ICON" "$DEST_ICON" + ICON_VALUE="$DEST_ICON" + success "Icon installed: $DEST_ICON" +else + info "No icon provided — using default: application-x-executable" +fi + +# ── Create .desktop file ────────────────────────────────────────────────────── +cat > "$DESKTOP_FILE" </dev/null; then + update-desktop-database "$DESKTOP_DB_DIR" 2>/dev/null && \ + success "Desktop database updated" +fi + +# Update icon cache (non-critical) +if command -v gtk-update-icon-cache &>/dev/null; then + ICON_THEME_DIR="$(dirname "$(dirname "$(dirname "$ICON_DIR")")")" + gtk-update-icon-cache -q "$ICON_THEME_DIR" 2>/dev/null || true +fi + +# ── Summary ─────────────────────────────────────────────────────────────────── +echo "" +echo -e "${BLD}── Summary ──────────────────────────────${RST}" +echo -e " Name : ${BLD}$DISPLAY_NAME${RST}" +echo -e " Binary : $DEST_BIN" +echo -e " .desktop : $DESKTOP_FILE" +echo -e " Icon : $ICON_VALUE" +echo -e " Scope : $(if $GLOBAL; then echo 'system-wide'; else echo "user ($USER)"; fi)" +echo "" +echo -e " Run with : ${BLD}$BIN_NAME${RST}" +echo -e "${BLD}─────────────────────────────────────────${RST}"