311 lines
12 KiB
Bash
Executable File
311 lines
12 KiB
Bash
Executable File
#!/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}"
|