chore: init
This commit is contained in:
commit
300ef54a91
3 files changed
+315
No files matched your search
@@ -0,0 +1,2 @@
|
||||
.idea/
|
||||
.venv/
|
||||
@@ -0,0 +1,3 @@
|
||||
# Adopt
|
||||
|
||||
Приложение для установки бинарных файлов и программных пакетов без инсталлятора. Интегрирует бинарник в ~/.local и создает для него Desktop файл.
|
||||
Executable
+310
@@ -0,0 +1,310 @@
|
||||
#!/usr/bin/env bash
|
||||
# install-app — install executables (AppImage/ELF/script) to PATH with .desktop integration
|
||||
# Usage: install-app [OPTIONS] <executable>
|
||||
|
||||
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 <<EOF
|
||||
${BLD}install-app${RST} — install executables with .desktop menu integration
|
||||
|
||||
${BLD}USAGE${RST}
|
||||
install-app [OPTIONS] <executable>
|
||||
|
||||
${BLD}OPTIONS${RST}
|
||||
-g, --global Install system-wide (/usr/local/bin, /usr/share/)
|
||||
Requires sudo / root
|
||||
-i, --icon <file> Icon file (png/svg/xpm)
|
||||
-n, --name <name> Display name (default: derived from filename)
|
||||
-c, --category <cat> .desktop category (default: Utility)
|
||||
--update <new_file> 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 <name> 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" <<EOF
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=$DISPLAY_NAME
|
||||
Exec=$DEST_BIN %F
|
||||
Icon=$ICON_VALUE
|
||||
Categories=$CATEGORY;
|
||||
Terminal=false
|
||||
StartupNotify=true
|
||||
EOF
|
||||
|
||||
success ".desktop created: $DESKTOP_FILE"
|
||||
|
||||
# ── Update desktop database ───────────────────────────────────────────────────
|
||||
if command -v update-desktop-database &>/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}"
|
||||
Reference in new issue
Block a user