From 7e611c3cd88c351ca86e63e90b7608cbfd572ab2 Mon Sep 17 00:00:00 2001 From: Frank Vitrano <90512559+WHC2006@users.noreply.github.com> Date: Sun, 19 Jul 2026 14:06:49 +0800 Subject: [PATCH] first upload --- LICENSE | 21 +++ README.md | 47 ++++++ completions/miaur.bash | 28 ++++ completions/miaur.zsh | 27 ++++ config/miaur.conf.example | 18 +++ install.sh | 139 ++++++++++++++++++ lib/aur_rpc.sh | 116 +++++++++++++++ lib/build.sh | 201 +++++++++++++++++++++++++ lib/cli.sh | 202 ++++++++++++++++++++++++++ lib/clone.sh | 48 ++++++ lib/common.sh | 108 ++++++++++++++ lib/config.sh | 76 ++++++++++ lib/deps.sh | 156 ++++++++++++++++++++ lib/dispatch.sh | 298 ++++++++++++++++++++++++++++++++++++++ lib/review.sh | 57 ++++++++ lib/upgrade.sh | 97 +++++++++++++ miaur | 60 ++++++++ 17 files changed, 1699 insertions(+) create mode 100644 LICENSE create mode 100644 completions/miaur.bash create mode 100644 completions/miaur.zsh create mode 100644 config/miaur.conf.example create mode 100644 install.sh create mode 100644 lib/aur_rpc.sh create mode 100644 lib/build.sh create mode 100644 lib/cli.sh create mode 100644 lib/clone.sh create mode 100644 lib/common.sh create mode 100644 lib/config.sh create mode 100644 lib/deps.sh create mode 100644 lib/dispatch.sh create mode 100644 lib/review.sh create mode 100644 lib/upgrade.sh create mode 100644 miaur diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b4a2388 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 miaur contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 4bbd769..a0e94c1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,49 @@ # miaur + A lightweight AUR helper written in pure Bash. No helper compilation required. + +Its everyday commands follow paru, including `-Syu`, `-S`, `-Ss`, `-Qua`, and `-G`. + +## Install + +```bash +git clone https://github.com/WHC2006/miaur.git +cd miaur +./install.sh +``` + +One command: + +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/WHC2006/miaur/main/install.sh) +``` + +Install for the current user under `~/.local`: + +```bash +./install.sh --user +``` + +The installer adds the required packages automatically: `base-devel`, `git`, `curl`, and `jq`. + +## Usage + +```bash +miaur # Upgrade everything (same as -Syu) +miaur foo # Search and select packages interactively +miaur -S # Install a package +miaur -Ss # Search packages +miaur -Qua # List available AUR upgrades +miaur -Sua # Upgrade AUR packages only +miaur -G # Download a PKGBUILD +``` + +Run `miaur -h` for more options. + +## Configuration + +The optional configuration file is `~/.config/miaur/miaur.conf`. See `config/miaur.conf.example`. + +## License + +MIT diff --git a/completions/miaur.bash b/completions/miaur.bash new file mode 100644 index 0000000..5f1710c --- /dev/null +++ b/completions/miaur.bash @@ -0,0 +1,28 @@ +# bash completion for miaur +_miaur() { + local cur prev + COMPREPLY=() + cur=${COMP_WORDS[COMP_CWORD]} + prev=${COMP_WORDS[COMP_CWORD - 1]} + + local ops='-S -Syu -Sua -Ss -Si -Sc -Q -Qua -Qm -R -Rns -G -Gp -B -Bi -h -V' + local longs='--aur --repo --skipreview --cleanafter --removemake --noremovermake --bottomup --topdown --clonedir --mflags --sudoloop --nosudoloop --help --version' + + case "$prev" in + -S | -Si | -Ss | -G | -Gp | -R | -Rns | -Qi) + COMPREPLY=($(compgen -W "$(pacman -Ssq "^${cur}" 2>/dev/null | head -50)" -- "$cur")) + return + ;; + --clonedir) + COMPREPLY=($(compgen -d -- "$cur")) + return + ;; + esac + + if [[ $cur == -* ]]; then + COMPREPLY=($(compgen -W "$ops $longs" -- "$cur")) + else + COMPREPLY=($(compgen -W "$(pacman -Ssq "^${cur}" 2>/dev/null | head -40)" -- "$cur")) + fi +} +complete -F _miaur miaur diff --git a/completions/miaur.zsh b/completions/miaur.zsh new file mode 100644 index 0000000..6f14694 --- /dev/null +++ b/completions/miaur.zsh @@ -0,0 +1,27 @@ +#compdef miaur + +_miaur() { + local -a ops + ops=( + '-Syu:upgrade repository and AUR packages' + '-Sua:upgrade AUR packages only' + '-Ss:search packages' + '-Si:show package information' + '-S:install a package' + '-Qua:list AUR upgrades' + '-G:download a PKGBUILD' + '-Gp:print a PKGBUILD' + '-Bi:build and install' + '-Sc:clean caches' + '-Rns:remove a package' + '--aur:use the AUR only' + '--repo:use repositories only' + '--skipreview:skip PKGBUILD review' + '--bottomup:show AUR results first' + '--help:show help' + ) + _arguments '*:package:_files' \ + "${ops[@]}" +} + +_miaur diff --git a/config/miaur.conf.example b/config/miaur.conf.example new file mode 100644 index 0000000..fb6cb9f --- /dev/null +++ b/config/miaur.conf.example @@ -0,0 +1,18 @@ +# miaur configuration example +# Copy this file to ~/.config/miaur/miaur.conf + +# AurUrl = https://aur.archlinux.org +# AurRpcUrl = https://aur.archlinux.org/rpc/v5 +# CloneDir = ~/.cache/miaur + +SkipReview = false +CleanAfter = false +RemoveMake = true +BottomUp = false +SudoLoop = true + +# MFlags = -si +# SortBy = votes +# SearchBy = name-desc +# Editor = nvim +# Bat = bat diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..210ce6e --- /dev/null +++ b/install.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# miaur one-click installer +# Usage: +# ./install.sh +# ./install.sh --user +# ./install.sh --uninstall +# bash <(curl -fsSL https://raw.githubusercontent.com//miaur/main/install.sh) +set -euo pipefail + +# Override when forking: MIAUR_REPO=https://github.com/you/miaur.git ./install.sh +REPO_URL="${MIAUR_REPO:-https://github.com/WHC2006/miaur.git}" +BRANCH="${MIAUR_BRANCH:-main}" + +USER_INSTALL=0 +UNINSTALL=0 + +for arg in "$@"; do + case "$arg" in + --user) USER_INSTALL=1 ;; + --uninstall) UNINSTALL=1 ;; + -h | --help) + cat <<'EOF' +Usage: install.sh [--user] [--uninstall] + + (default) Install system-wide to /usr/local (needs sudo) + --user Install to ~/.local (no sudo for the link) + --uninstall Remove installed miaur +EOF + exit 0 + ;; + *) + echo "Unknown option: $arg" >&2 + exit 1 + ;; + esac +done + +if [[ $USER_INSTALL -eq 1 ]]; then + PREFIX="${HOME}/.local" + SUDO= +else + PREFIX=/usr/local + SUDO=sudo +fi + +SHARE_DIR="${PREFIX}/share/miaur" +BIN_LINK="${PREFIX}/bin/miaur" + +die() { echo "error: $*" >&2; exit 1; } + +need_root_ok() { + if [[ $USER_INSTALL -eq 0 && ${EUID:-$(id -u)} -ne 0 ]]; then + command -v sudo >/dev/null 2>&1 || die "sudo is required" + sudo -v || die "sudo authentication failed" + fi +} + +uninstall() { + need_root_ok + echo "==> Uninstalling miaur" + $SUDO rm -f "$BIN_LINK" + $SUDO rm -rf "$SHARE_DIR" + echo "Done: removed $BIN_LINK and $SHARE_DIR" +} + +install_deps() { + command -v pacman >/dev/null 2>&1 || die "Arch Linux is required (pacman not found)" + echo "==> Installing dependencies" + if [[ ${EUID:-$(id -u)} -eq 0 ]]; then + pacman -S --needed --noconfirm base-devel git curl jq + else + sudo pacman -S --needed --noconfirm base-devel git curl jq + fi +} + +# Resolve source tree: current repo, or clone to temp +resolve_src() { + local here + here=$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd) + if [[ -f $here/miaur && -d $here/lib ]]; then + SRC=$here + return 0 + fi + + echo "==> Cloning ${REPO_URL} (${BRANCH})" + TMP=$(mktemp -d) + trap 'rm -rf "${TMP:-}"' EXIT + git clone --depth 1 --branch "$BRANCH" "$REPO_URL" "$TMP/miaur" \ + || die "clone failed; set MIAUR_REPO to the repository URL" + SRC=$TMP/miaur +} + +do_install() { + need_root_ok + install_deps + resolve_src + + [[ -f $SRC/miaur && -d $SRC/lib ]] || die "incomplete source tree: $SRC" + + echo "==> Installing to ${SHARE_DIR}" + $SUDO mkdir -p "$SHARE_DIR" "${PREFIX}/bin" + $SUDO rm -rf "$SHARE_DIR" + $SUDO mkdir -p "$SHARE_DIR" + $SUDO cp -a "$SRC/miaur" "$SRC/lib" "$SHARE_DIR/" + if [[ -d $SRC/completions ]]; then + $SUDO cp -a "$SRC/completions" "$SHARE_DIR/" + fi + if [[ -d $SRC/config ]]; then + $SUDO cp -a "$SRC/config" "$SHARE_DIR/" + fi + $SUDO chmod 755 "$SHARE_DIR/miaur" + $SUDO ln -sfn "$SHARE_DIR/miaur" "$BIN_LINK" + + # Optional bash completion (system or user) + if [[ -d $SRC/completions ]]; then + if [[ $USER_INSTALL -eq 1 ]]; then + mkdir -p "${HOME}/.local/share/bash-completion/completions" + cp -f "$SRC/completions/miaur.bash" \ + "${HOME}/.local/share/bash-completion/completions/miaur" 2>/dev/null || true + else + if [[ -d /usr/share/bash-completion/completions ]]; then + $SUDO cp -f "$SRC/completions/miaur.bash" \ + /usr/share/bash-completion/completions/miaur 2>/dev/null || true + fi + fi + fi + + echo + echo "Done: $(command -v miaur 2>/dev/null || echo "$BIN_LINK")" + "$BIN_LINK" -V + echo + echo "Try: miaur -Ss foo" +} + +if [[ $UNINSTALL -eq 1 ]]; then + uninstall +else + do_install +fi diff --git a/lib/aur_rpc.sh b/lib/aur_rpc.sh new file mode 100644 index 0000000..712fe88 --- /dev/null +++ b/lib/aur_rpc.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +# AUR RPC helpers (v5) using curl + jq + +aur_rpc_get() { + local url=$1 + local curl_bin + curl_bin=$(resolve_curl) + "$curl_bin" -fsSL --connect-timeout 15 --max-time 60 "$url" +} + +# Search AUR; prints: name|version|votes|popularity|desc (one per line) +aur_search() { + local query=$1 + local by=${2:-$MIAUR_SEARCHBY} + local encoded + encoded=$(printf '%s' "$query" | jq -sRr @uri) + local json + json=$(aur_rpc_get "${MIAUR_AURRPC}/search/${encoded}?by=${by}") || return 1 + local sort=${MIAUR_SORTBY:-votes} + local jq_sort + case "$sort" in + votes) jq_sort='sort_by(.NumVotes) | reverse' ;; + popularity) jq_sort='sort_by(.Popularity) | reverse' ;; + name) jq_sort='sort_by(.Name)' ;; + *) jq_sort='sort_by(.NumVotes) | reverse' ;; + esac + jq -r --argjson err 0 " + if .type == \"error\" then empty + else (.results // []) | ${jq_sort} | .[] | + [.Name, .Version, (.NumVotes|tostring), (.Popularity|tostring), (.Description // \"\")] | join(\"|\") + end + " <<<"$json" +} + +# Info for one package; prints JSON object or empty +aur_info_json() { + local name=$1 + local encoded + encoded=$(printf '%s' "$name" | jq -sRr @uri) + local json + json=$(aur_rpc_get "${MIAUR_AURRPC}/info?arg[]=${encoded}") || return 1 + jq -c '.results[0] // empty' <<<"$json" +} + +# Multi-info: args are package names. Prints JSON array of results. +aur_multiinfo_json() { + local args=() + local n + for n in "$@"; do + args+=(--data-urlencode "arg[]=${n}") + done + [[ ${#args[@]} -eq 0 ]] && { echo '[]'; return 0; } + local json + local curl_bin + curl_bin=$(resolve_curl) + json=$("$curl_bin" -fsSL --connect-timeout 15 --max-time 60 -G "${MIAUR_AURRPC}/info" "${args[@]}") || return 1 + jq -c '.results // []' <<<"$json" +} + +# Check if package exists on AUR (exit 0 if yes) +aur_exists() { + local j + j=$(aur_info_json "$1") || return 1 + [[ -n $j && $j != null ]] +} + +# Print human-readable info for a package (like pacman -Si for AUR) +aur_print_info() { + local name=$1 + local j + j=$(aur_info_json "$name") || return 1 + [[ -z $j || $j == null ]] && return 1 + jq -r ' + "Repository : aur", + "Name : \(.Name)", + "Version : \(.Version)", + "Description : \(.Description // "")", + "URL : \(.URL // "")", + "AUR URL : https://aur.archlinux.org/packages/\(.Name)", + "Groups : \((.Groups // []) | join(" "))", + "Depends On : \((.Depends // []) | join(" "))", + "Make Depends : \((.MakeDepends // []) | join(" "))", + "Check Depends : \((.CheckDepends // []) | join(" "))", + "Optional Deps : \((.OptDepends // []) | join(" "))", + "Provides : \((.Provides // []) | join(" "))", + "Conflicts With : \((.Conflicts // []) | join(" "))", + "Replaces : \((.Replaces // []) | join(" "))", + "Maintainer : \(.Maintainer // "(orphan)")", + "Votes : \(.NumVotes)", + "Popularity : \(.Popularity)", + "Last Modified : \(.LastModified)", + "Out Of Date : \(if .OutOfDate then (.OutOfDate|tostring) else "No" end)" + ' <<<"$j" +} + +# Print search line for AUR result: aur/name version (votes) desc +aur_format_search_line() { + local name=$1 ver=$2 votes=$3 pop=$4 desc=$5 + local installed= + if is_pkg_installed "$name"; then + local localver + localver=$(pkg_local_version "$name") + if [[ $localver == "$ver" ]]; then + installed=" ${C_GREEN}[installed]${C_RESET}" + else + installed=" ${C_YELLOW}[installed: ${localver}]${C_RESET}" + fi + fi + printf '%saur/%s%s %s%s (%s)%s\n %s\n' \ + "${C_BOLD}${C_CYAN}" "$name" "${C_RESET}" \ + "${C_BOLD}${C_GREEN}${ver}${C_RESET}" \ + "$installed" \ + "$votes" \ + "" \ + "$desc" +} diff --git a/lib/build.sh b/lib/build.sh new file mode 100644 index 0000000..47e2c5a --- /dev/null +++ b/lib/build.sh @@ -0,0 +1,201 @@ +#!/usr/bin/env bash +# Build and install AUR packages + +# Build package in dir. install=1 to install after build. +# asdeps=1 to mark as dependency. +build_pkg() { + local dir=$1 + local do_install=${2:-1} + local asdeps=${3:-0} + local name + name=$(basename "$dir") + + msg "Building $name..." + ( + cd "$dir" || exit 1 + # Clear previous package files to avoid installing stale ones + rm -f ./*.pkg.tar.* 2>/dev/null || true + local -a mflags=() + # shellcheck disable=SC2206 + mflags=(${MIAUR_MFLAGS}) + # If MFLAGS contains -si we handle install ourselves more carefully + local -a clean_flags=() + local f + for f in "${mflags[@]}"; do + case "$f" in + -i | --install | -si | -is) ;; + *) clean_flags+=("$f") ;; + esac + done + # Always sync deps for repo side via makepkg -s when needed + ${MIAUR_MAKEPKG} -s --noconfirm "${clean_flags[@]}" || exit 1 + ) || die "build failed: $name" + + if [[ $do_install -eq 1 ]]; then + local -a pkgs=() + mapfile -t pkgs < <(find "$dir" -maxdepth 1 -type f \( -name '*.pkg.tar.zst' -o -name '*.pkg.tar.xz' -o -name '*.pkg.tar.gz' \) ! -name '*-debug-*' | sort) + [[ ${#pkgs[@]} -eq 0 ]] && die "no build artifacts found: $name" + msg "Installing $name..." + local -a uflags=(-U --noconfirm) + [[ $asdeps -eq 1 ]] && uflags+=(--asdeps) + as_root ${MIAUR_PACMAN} "${uflags[@]}" "${pkgs[@]}" || die "installation failed: $name" + fi + + if [[ ${MIAUR_CLEANAFTER:-0} -eq 1 ]]; then + msg2 "Cleaning build directory: $dir" + rm -rf "$dir" + fi +} + +# Full install pipeline for AUR package names (and their AUR deps) +# Targets are package names. +install_aur_packages() { + local -a targets=("$@") + [[ ${#targets[@]} -eq 0 ]] && return 0 + + need_cmd curl jq git makepkg pacman + + sudoloop_start + + msg "Resolving dependencies..." + local -a order=() + mapfile -t order < <(aur_resolve_order "${targets[@]}") + [[ ${#order[@]} -eq 0 ]] && die "no packages to install" + + # Collect all repo deps across the graph + local -a repo_deps=() + local pkg dir line + declare -A repo_seen=() + for pkg in "${order[@]}"; do + dir=$(pkg_clone_dir "$pkg") + [[ -d $dir ]] || dir=$(aur_clone "$pkg") + while IFS= read -r line; do + case "$line" in + repo\|*) + local rn=${line#repo|} + [[ -n ${repo_seen[$rn]+x} ]] && continue + repo_seen[$rn]=1 + repo_deps+=("$rn") + ;; + esac + done < <(collect_deps_for_dir "$dir" 1 1) + done + + install_repo_deps "${repo_deps[@]}" + + # Review all packages first (like paru) + local -a build_list=() + for pkg in "${order[@]}"; do + dir=$(pkg_clone_dir "$pkg") + [[ -d $dir ]] || dir=$(aur_clone "$pkg") + local rc=0 + review_pkgbuild "$dir" "$pkg" || rc=$? + if [[ $rc -eq 1 ]]; then + die "installation cancelled" + elif [[ $rc -eq 2 ]]; then + warn "Skipping $pkg" + continue + fi + build_list+=("$pkg") + done + + [[ ${#build_list[@]} -eq 0 ]] && die "no packages to build" + + # Explicit install targets (deps marked --asdeps) + declare -A is_target=() + for pkg in "${targets[@]}"; do + is_target[$pkg]=1 + done + + # Track make deps for optional removal (names from srcinfo) + local -a make_pkgs=() + declare -A make_seen=() + if [[ ${MIAUR_REMOVEMAKE:-0} -eq 1 ]]; then + for pkg in "${build_list[@]}"; do + dir=$(pkg_clone_dir "$pkg") + while IFS= read -r line; do + case "$line" in + makedepend\|*) + local mn + mn=$(dep_name "${line#makedepend|}") + [[ -n ${is_target[$mn]+x} ]] && continue + [[ -n ${make_seen[$mn]+x} ]] && continue + make_seen[$mn]=1 + make_pkgs+=("$mn") + ;; + esac + done < <(srcinfo_dump "$dir") + done + fi + + for pkg in "${build_list[@]}"; do + dir=$(pkg_clone_dir "$pkg") + local asdeps=0 + [[ -z ${is_target[$pkg]+x} ]] && asdeps=1 + # Skip if already up to date + if is_pkg_installed "$pkg"; then + local localver aurver + localver=$(pkg_local_version "$pkg") + aurver=$(jq -r '.Version // empty' <<<"$(aur_info_json "$pkg")" 2>/dev/null || true) + if [[ -n $aurver && $localver == "$aurver" && -z ${is_target[$pkg]+x} ]]; then + msg2 "$pkg is up to date ($localver)" + continue + fi + fi + build_pkg "$dir" 1 "$asdeps" + done + + # Remove make depends that were installed as deps and not needed + if [[ ${MIAUR_REMOVEMAKE:-0} -eq 1 && ${#make_pkgs[@]} -gt 0 ]]; then + local -a to_rm=() + local m + for m in "${make_pkgs[@]}"; do + # Only remove if installed as dependency and not required + if pacman -Qdtq 2>/dev/null | grep -qx "$m"; then + to_rm+=("$m") + fi + done + if [[ ${#to_rm[@]} -gt 0 ]]; then + msg "Removing make dependencies: ${to_rm[*]}" + as_root ${MIAUR_PACMAN} -Rns --noconfirm "${to_rm[@]}" || warn "failed to remove make dependencies" + fi + fi + + sudoloop_stop + msg "Done" +} + +# Build from local directories containing PKGBUILD +build_local_dirs() { + local do_install=${1:-0} + shift + local -a dirs=("$@") + local d + for d in "${dirs[@]}"; do + [[ -d $d ]] || die "directory does not exist: $d" + [[ -f $d/PKGBUILD ]] || die "PKGBUILD not found: $d" + # Resolve AUR deps of local pkgbuild + local -a aur_deps=() + local line + while IFS= read -r line; do + case "$line" in + aur\|*) aur_deps+=("${line#aur|}") ;; + missing\|*) die "unable to resolve dependency: ${line#missing|}" ;; + repo\|*) ;; + esac + done < <(collect_deps_for_dir "$d" 1 1) + if [[ ${#aur_deps[@]} -gt 0 ]]; then + install_aur_packages "${aur_deps[@]}" + fi + # Install repo deps + local -a rd=() + while IFS= read -r line; do + case "$line" in + repo\|*) rd+=("${line#repo|}") ;; + esac + done < <(collect_deps_for_dir "$d" 1 1) + install_repo_deps "${rd[@]}" + review_pkgbuild "$d" "$(basename "$d")" || die "cancelled" + build_pkg "$d" "$do_install" 0 + done +} diff --git a/lib/cli.sh b/lib/cli.sh new file mode 100644 index 0000000..df5042e --- /dev/null +++ b/lib/cli.sh @@ -0,0 +1,202 @@ +#!/usr/bin/env bash +# CLI parsing — paru-like short option clusters + +# Globals set by parse_args +OP= # S Q R G B P D F T U none interactive +OP_REFRESH=0 # y +OP_UPGRADE=0 # u +OP_SEARCH=0 # s (count) +OP_INFO=0 # i +OP_CLEAN=0 # c +OP_AUR_ONLY=0 # a (with S/Q) +OP_PRINT=0 # -p for -G +OP_BUILD_INSTALL=0 # -i for -B +TARGETS=() +PACMAN_EXTRA=() # leftover short flags for pacman passthrough +DO_PASSTHRU=0 + +print_help() { + cat <<'EOF' +Usage: + miaur + miaur [...] + miaur + +miaur is a pure-shell AUR helper with paru-like everyday commands. + +Pacman operations extended with AUR support: + miaur {-h --help} + miaur {-V --version} + miaur {-Q --query} [options] [packages] + miaur {-R --remove} [options] + miaur {-S --sync} [options] [packages] + miaur {-U --upgrade} [options] + +Additional operations: + miaur {-G --getpkgbuild} [packages] + miaur {-B --build} [directories] + +With no arguments, miaur runs 'miaur -Syu'. + +Common commands: + miaur -Syu Upgrade repository and AUR packages + miaur -Sua Upgrade AUR packages only + miaur -S Install from a repository or the AUR + miaur -Ss Search packages + miaur -Si Show package information + miaur -Qua List available AUR upgrades + miaur -G Download a PKGBUILD + miaur -Gp Print a PKGBUILD + miaur -Bi . Build and install a local PKGBUILD + +Options: + -a, --aur Assume targets are from the AUR + --repo Assume targets are from repositories + --skipreview Skip PKGBUILD review + --cleanafter Remove build sources after installation + --[no]removemake Remove make dependencies after installation + --bottomup Show AUR results first + --topdown Show repository results first (default) + --clonedir Set the PKGBUILD clone directory + --mflags Pass additional flags to makepkg + --sudoloop Keep sudo credentials alive (default) + --nosudoloop Disable the sudo keep-alive loop +EOF +} + +parse_args() { + if [[ $# -eq 0 ]]; then + OP=S + OP_REFRESH=1 + OP_UPGRADE=1 + return 0 + fi + + # No leading dash → interactive search + if [[ $1 != -* ]]; then + OP=interactive + TARGETS=("$@") + return 0 + fi + + local -a raw=("$@") + local i=0 + while ((i < ${#raw[@]})); do + local arg=${raw[$i]} + case "$arg" in + -h | --help) + print_help + exit 0 + ;; + -V | --version) + printf 'miaur v%s\n' "$MIAUR_VERSION" + exit 0 + ;; + --aur) + OP_AUR_ONLY=1 + MIAUR_MODE=aur + ;; + --repo) + MIAUR_MODE=repo + ;; + --skipreview) MIAUR_SKIPREVIEW=1 ;; + --review) MIAUR_SKIPREVIEW=0 ;; + --cleanafter) MIAUR_CLEANAFTER=1 ;; + --nocleanafter) MIAUR_CLEANAFTER=0 ;; + --removemake) MIAUR_REMOVEMAKE=1 ;; + --noremovermake) MIAUR_REMOVEMAKE=0 ;; + --bottomup) MIAUR_BOTTOMUP=1 ;; + --topdown) MIAUR_BOTTOMUP=0 ;; + --sudoloop) MIAUR_SUDOLOOP=1 ;; + --nosudoloop) MIAUR_SUDOLOOP=0 ;; + --clonedir) + i=$((i + 1)) + MIAUR_CLONEDIR=${raw[$i]:-} + [[ -n $MIAUR_CLONEDIR ]] || die "--clonedir requires a path" + ;; + --clonedir=*) MIAUR_CLONEDIR=${arg#*=} ;; + --mflags) + i=$((i + 1)) + MIAUR_MFLAGS=${raw[$i]:-} + ;; + --mflags=*) MIAUR_MFLAGS=${arg#*=} ;; + --mode) + i=$((i + 1)) + MIAUR_MODE=${raw[$i]:-} + ;; + --mode=*) MIAUR_MODE=${arg#*=} ;; + --sync) OP=S ;; + --query) OP=Q ;; + --remove) + OP=R + DO_PASSTHRU=1 + ;; + --getpkgbuild) OP=G ;; + --build) OP=B ;; + --database | --files | --deptest | --upgrade) + DO_PASSTHRU=1 + ;; + --) + i=$((i + 1)) + while ((i < ${#raw[@]})); do + TARGETS+=("${raw[$i]}") + i=$((i + 1)) + done + break + ;; + -*) + local cluster=${arg:1} + local j c + for ((j = 0; j < ${#cluster}; j++)); do + c=${cluster:j:1} + case "$c" in + S) OP=S ;; + Q) OP=Q ;; + R) + OP=R + DO_PASSTHRU=1 + ;; + G) OP=G ;; + B) OP=B ;; + D | F | T | U) DO_PASSTHRU=1 ;; + y) OP_REFRESH=1 ;; + u) OP_UPGRADE=1 ;; + s) OP_SEARCH=$((OP_SEARCH + 1)) ;; + i) + if [[ $OP == B ]]; then + OP_BUILD_INSTALL=1 + else + OP_INFO=1 + fi + ;; + c) OP_CLEAN=1 ;; + a) + OP_AUR_ONLY=1 + MIAUR_MODE=aur + ;; + p) + if [[ $OP == G ]]; then + OP_PRINT=1 + else + PACMAN_EXTRA+=("$c") + fi + ;; + h) + print_help + exit 0 + ;; + V) + printf 'miaur v%s\n' "$MIAUR_VERSION" + exit 0 + ;; + *) PACMAN_EXTRA+=("$c") ;; + esac + done + ;; + *) + TARGETS+=("$arg") + ;; + esac + i=$((i + 1)) + done +} diff --git a/lib/clone.sh b/lib/clone.sh new file mode 100644 index 0000000..b60121c --- /dev/null +++ b/lib/clone.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Clone / update AUR package sources + +pkg_clone_dir() { + local name=$1 + printf '%s/%s' "${MIAUR_CLONEDIR}" "$name" +} + +# Clone or pull package; prints absolute path to clone dir +aur_clone() { + local name=$1 + local dir + dir=$(pkg_clone_dir "$name") + mkdir -p "$MIAUR_CLONEDIR" + if [[ -d $dir/.git ]]; then + msg2 "Updating $name..." + if ! (cd "$dir" && ${MIAUR_GIT} pull --ff-only --quiet); then + warn "Cannot fast-forward $name; cloning it again" + rm -rf "$dir" + ${MIAUR_GIT} clone --quiet "${MIAUR_AURURL}/${name}.git" "$dir" || die "clone failed: $name" + fi + else + rm -rf "$dir" + msg2 "Cloning $name..." + ${MIAUR_GIT} clone --quiet "${MIAUR_AURURL}/${name}.git" "$dir" || die "clone failed: $name" + fi + [[ -f $dir/PKGBUILD ]] || die "PKGBUILD not found after cloning: $name" + printf '%s\n' "$dir" +} + +# Clean clone cache for one package or all +aur_clean_cache() { + local target=${1:-} + if [[ -n $target ]]; then + local dir + dir=$(pkg_clone_dir "$target") + if [[ -d $dir ]]; then + msg "Cleaning cache: $dir" + rm -rf "$dir" + fi + else + if [[ -d $MIAUR_CLONEDIR ]]; then + msg "Cleaning AUR cache: $MIAUR_CLONEDIR" + # Keep VCS src dirs? Plan says clean clone cache — remove package dirs + find "$MIAUR_CLONEDIR" -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} + + fi + fi +} diff --git a/lib/common.sh b/lib/common.sh new file mode 100644 index 0000000..abe77cd --- /dev/null +++ b/lib/common.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2034 +# Common utilities for miaur + +MIAUR_VERSION="0.1.0" + +# Colors (respect pacman Color / NO_COLOR / non-tty) +_miaur_color_enabled() { + [[ -n ${NO_COLOR:-} ]] && return 1 + [[ ! -t 1 ]] && return 1 + if [[ -f /etc/pacman.conf ]] && grep -Eq '^[[:space:]]*Color([[:space:]]|$)' /etc/pacman.conf 2>/dev/null; then + return 0 + fi + # Default: enable on tty even if pacman Color is commented + return 0 +} + +if _miaur_color_enabled; then + C_RED=$'\033[31m' + C_GREEN=$'\033[32m' + C_YELLOW=$'\033[33m' + C_BLUE=$'\033[34m' + C_CYAN=$'\033[36m' + C_BOLD=$'\033[1m' + C_DIM=$'\033[2m' + C_RESET=$'\033[0m' +else + C_RED= C_GREEN= C_YELLOW= C_BLUE= C_CYAN= C_BOLD= C_DIM= C_RESET= +fi + +msg() { printf '%s::%s %s\n' "${C_BOLD}${C_BLUE}" "${C_RESET}" "$*" >&2; } +msg2() { printf '%s ->%s %s\n' "${C_BOLD}${C_BLUE}" "${C_RESET}" "$*" >&2; } +warn() { printf '%s==>%s WARNING: %s\n' "${C_BOLD}${C_YELLOW}" "${C_RESET}" "$*" >&2; } +error() { printf '%s==>%s ERROR: %s\n' "${C_BOLD}${C_RED}" "${C_RESET}" "$*" >&2; } +die() { error "$*"; exit 1; } + +need_cmd() { + local c + for c in "$@"; do + command -v "$c" >/dev/null 2>&1 || die "required command not found: $c" + done +} + +# Prefer system curl — some user PATH wrappers break AUR HTTPS +resolve_curl() { + if [[ -n ${MIAUR_CURL:-} ]]; then + printf '%s' "$MIAUR_CURL" + elif [[ -x /usr/bin/curl ]]; then + printf '%s' /usr/bin/curl + else + command -v curl + fi +} + +# Strip version constraints from a dep string: foo>=1.0 -> foo +dep_name() { + local d=$1 + d=${d%%[><=]*} + d=${d%%:*} + printf '%s' "$d" +} + +vercmp_safe() { + # pacman --print returns 0 if equal, 1 if first >, -1 if second > + /usr/bin/vercmp "$1" "$2" +} + +is_pkg_installed() { + pacman -Q "$1" &>/dev/null +} + +pkg_local_version() { + pacman -Q "$1" 2>/dev/null | awk '{print $2}' +} + +# Run command as root via sudo +as_root() { + if [[ ${EUID:-$(id -u)} -eq 0 ]]; then + "$@" + else + ${MIAUR_SUDO:-sudo} ${MIAUR_SUDOFLAGS:-} "$@" + fi +} + +# Keep sudo credentials alive in background +_sudo_loop_pid= +sudoloop_start() { + [[ ${MIAUR_SUDOLOOP:-0} -eq 1 ]] || return 0 + [[ ${EUID:-$(id -u)} -eq 0 ]] && return 0 + ${MIAUR_SUDO:-sudo} -v || return 1 + ( + while true; do + sleep 60 + ${MIAUR_SUDO:-sudo} -v || exit + done + ) & + _sudo_loop_pid=$! +} + +sudoloop_stop() { + if [[ -n ${_sudo_loop_pid:-} ]]; then + kill "$_sudo_loop_pid" 2>/dev/null || true + wait "$_sudo_loop_pid" 2>/dev/null || true + _sudo_loop_pid= + fi +} + +trap sudoloop_stop EXIT diff --git a/lib/config.sh b/lib/config.sh new file mode 100644 index 0000000..5e2fb39 --- /dev/null +++ b/lib/config.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# Config defaults and loader for miaur + +# Defaults +MIAUR_AURURL="${MIAUR_AURURL:-https://aur.archlinux.org}" +MIAUR_AURRPC="${MIAUR_AURRPC:-https://aur.archlinux.org/rpc/v5}" +MIAUR_CLONEDIR="${MIAUR_CLONEDIR:-${XDG_CACHE_HOME:-$HOME/.cache}/miaur}" +MIAUR_SKIPREVIEW="${MIAUR_SKIPREVIEW:-0}" +MIAUR_CLEANAFTER="${MIAUR_CLEANAFTER:-0}" +MIAUR_REMOVEMAKE="${MIAUR_REMOVEMAKE:-1}" +MIAUR_BOTTOMUP="${MIAUR_BOTTOMUP:-0}" +MIAUR_SUDOLOOP="${MIAUR_SUDOLOOP:-1}" +MIAUR_MFLAGS="${MIAUR_MFLAGS:--si}" +MIAUR_SORTBY="${MIAUR_SORTBY:-votes}" +MIAUR_SEARCHBY="${MIAUR_SEARCHBY:-name-desc}" +MIAUR_MODE="${MIAUR_MODE:-}" # aur|repo|empty=both +MIAUR_SUDO="${MIAUR_SUDO:-sudo}" +MIAUR_SUDOFLAGS="${MIAUR_SUDOFLAGS:-}" +MIAUR_GIT="${MIAUR_GIT:-git}" +MIAUR_MAKEPKG="${MIAUR_MAKEPKG:-makepkg}" +MIAUR_PACMAN="${MIAUR_PACMAN:-pacman}" +MIAUR_BAT="${MIAUR_BAT:-bat}" +MIAUR_EDITOR="${VISUAL:-${EDITOR:-nano}}" + +_bool_val() { + case "${1,,}" in + 1 | true | yes | on) printf '1' ;; + 0 | false | no | off) printf '0' ;; + *) printf '%s' "$1" ;; + esac +} + +miaur_load_config() { + local conf="${XDG_CONFIG_HOME:-$HOME/.config}/miaur/miaur.conf" + [[ -f $conf ]] || return 0 + local line key val + while IFS= read -r line || [[ -n $line ]]; do + line=${line%%#*} + line=${line#"${line%%[![:space:]]*}"} + line=${line%"${line##*[![:space:]]}"} + [[ -z $line ]] && continue + key=${line%%=*} + val=${line#*=} + key=${key%"${key##*[![:space:]]}"} + key=${key#"${key%%[![:space:]]*}"} + val=${val#"${val%%[![:space:]]*}"} + val=${val%"${val##*[![:space:]]}"} + val=${val#\"} + val=${val%\"} + val=${val#\'} + val=${val%\'} + case "$key" in + AurUrl | AURURL) MIAUR_AURURL=$val ;; + AurRpcUrl | AURRPC) MIAUR_AURRPC=$val ;; + CloneDir) + MIAUR_CLONEDIR=$val + [[ $MIAUR_CLONEDIR == ~* ]] && MIAUR_CLONEDIR="${MIAUR_CLONEDIR/#\~/$HOME}" + ;; + SkipReview) MIAUR_SKIPREVIEW=$(_bool_val "$val") ;; + CleanAfter) MIAUR_CLEANAFTER=$(_bool_val "$val") ;; + RemoveMake) MIAUR_REMOVEMAKE=$(_bool_val "$val") ;; + BottomUp) MIAUR_BOTTOMUP=$(_bool_val "$val") ;; + SudoLoop) MIAUR_SUDOLOOP=$(_bool_val "$val") ;; + MFlags) MIAUR_MFLAGS=$val ;; + SortBy) MIAUR_SORTBY=$val ;; + SearchBy) MIAUR_SEARCHBY=$val ;; + Sudo) MIAUR_SUDO=$val ;; + SudoFlags) MIAUR_SUDOFLAGS=$val ;; + Git) MIAUR_GIT=$val ;; + Makepkg) MIAUR_MAKEPKG=$val ;; + Pacman) MIAUR_PACMAN=$val ;; + Bat) MIAUR_BAT=$val ;; + Editor) MIAUR_EDITOR=$val ;; + esac + done <"$conf" +} diff --git a/lib/deps.sh b/lib/deps.sh new file mode 100644 index 0000000..7f58a4a --- /dev/null +++ b/lib/deps.sh @@ -0,0 +1,156 @@ +#!/usr/bin/env bash +# Dependency resolution from .SRCINFO / makepkg --printsrcinfo + +# Parse srcinfo from directory into globals via stdout lines: +# pkgbase|NAME +# pkgname|NAME +# depend|DEP +# makedepend|DEP +# checkdepend|DEP +srcinfo_dump() { + local dir=$1 + ( + cd "$dir" || exit 1 + ${MIAUR_MAKEPKG} --printsrcinfo 2>/dev/null + ) | awk ' + /^pkgbase = / { print "pkgbase|" $3 } + /^[[:space:]]*pkgname = / { print "pkgname|" $3 } + /^[[:space:]]*depends = / { print "depend|" $3 } + /^[[:space:]]*makedepends = / { print "makedepend|" $3 } + /^[[:space:]]*checkdepends = / { print "checkdepend|" $3 } + ' +} + +# Returns 0 if dep is satisfied (installed or provided by installed pkg) +dep_satisfied() { + local dep name + name=$(dep_name "$1") + # pacman -T returns unsatisfied deps; empty means all ok + local out + out=$(pacman -T "$1" 2>/dev/null) || true + [[ -z $out ]] +} + +# Is package available in sync repos? +repo_has_pkg() { + pacman -Si "$1" &>/dev/null +} + +# Resolve whether a dep name is in AUR (not in repos, or --aur mode) +# Sets: REPO_DEPS[] AUR_DEPS[] (caller-managed arrays passed by name) +# resolve_one_dep name -> echoes "repo:name" or "aur:name" or "ok" or "missing:name" +resolve_one_dep() { + local raw=$1 + local name + name=$(dep_name "$raw") + if dep_satisfied "$raw"; then + echo "ok" + return 0 + fi + if [[ ${MIAUR_MODE:-} != aur ]] && repo_has_pkg "$name"; then + echo "repo:$name" + return 0 + fi + if [[ ${MIAUR_MODE:-} != repo ]] && aur_exists "$name"; then + echo "aur:$name" + return 0 + fi + # Try provides via pacman -Ssq exact? fallback AUR search by name + if [[ ${MIAUR_MODE:-} != repo ]] && aur_exists "$name"; then + echo "aur:$name" + return 0 + fi + echo "missing:$name" +} + +# Collect all deps for a cloned package dir into named arrays via stdout +# Format lines: repo|pkg / aur|pkg / missing|pkg +collect_deps_for_dir() { + local dir=$1 + local include_make=${2:-1} + local include_check=${3:-1} + local line kind val + declare -A seen=() + while IFS= read -r line; do + kind=${line%%|*} + val=${line#*|} + case "$kind" in + depend) ;; + makedepend) [[ $include_make -eq 1 ]] || continue ;; + checkdepend) [[ $include_check -eq 1 ]] || continue ;; + *) continue ;; + esac + local name + name=$(dep_name "$val") + [[ -n ${seen[$name]+x} ]] && continue + seen[$name]=1 + local r + r=$(resolve_one_dep "$val") + case "$r" in + ok) ;; + repo:*) echo "repo|${r#repo:}" ;; + aur:*) echo "aur|${r#aur:}" ;; + missing:*) echo "missing|${r#missing:}" ;; + esac + done < <(srcinfo_dump "$dir") +} + +# Recursively expand AUR deps for a list of package names. +# Prints ordered AUR package names (dependencies first). +# Uses DFS + topo-ish order. +aur_resolve_order() { + local -a roots=("$@") + declare -A visiting=() visited=() + local -a order=() + + _visit() { + local pkg=$1 + [[ -n ${visited[$pkg]+x} ]] && return 0 + if [[ -n ${visiting[$pkg]+x} ]]; then + warn "Dependency cycle detected at: $pkg" + return 0 + fi + visiting[$pkg]=1 + local dir + dir=$(aur_clone "$pkg") || die "failed to fetch dependency: $pkg" + local line + while IFS= read -r line; do + case "$line" in + aur\|*) + _visit "${line#aur|}" + ;; + missing\|*) + die "unable to resolve dependency ${line#missing|} required by $pkg" + ;; + esac + done < <(collect_deps_for_dir "$dir" 1 1) + unset 'visiting[$pkg]' + visited[$pkg]=1 + order+=("$pkg") + } + + local r + for r in "${roots[@]}"; do + _visit "$r" + done + printf '%s\n' "${order[@]}" +} + +# Install repo packages as deps +install_repo_deps() { + local -a pkgs=("$@") + [[ ${#pkgs[@]} -eq 0 ]] && return 0 + # Dedup + local -A u=() + local p + local -a list=() + for p in "${pkgs[@]}"; do + [[ -n ${u[$p]+x} ]] && continue + u[$p]=1 + is_pkg_installed "$p" && continue + list+=("$p") + done + [[ ${#list[@]} -eq 0 ]] && return 0 + msg "Installing repository dependencies: ${list[*]}" + as_root ${MIAUR_PACMAN} -S --needed --asdeps --noconfirm "${list[@]}" +} diff --git a/lib/dispatch.sh b/lib/dispatch.sh new file mode 100644 index 0000000..2e623f5 --- /dev/null +++ b/lib/dispatch.sh @@ -0,0 +1,298 @@ +#!/usr/bin/env bash +# Dispatch handlers for miaur operations + +# Reconstruct pacman argv from OP + PACMAN_EXTRA + targets for passthrough +pacman_passthru() { + local -a args=("$@") + as_root ${MIAUR_PACMAN} "${args[@]}" +} + +# Search repo + AUR +cmd_search() { + local -a terms=("$@") + [[ ${#terms[@]} -eq 0 ]] && die "specify a search query" + local query="${terms[*]}" + + local -a repo_lines=() aur_lines=() + + if [[ ${MIAUR_MODE:-} != aur ]]; then + mapfile -t repo_lines < <(${MIAUR_PACMAN} -Ss "$query" 2>/dev/null || true) + fi + + if [[ ${MIAUR_MODE:-} != repo ]]; then + local line name ver votes pop desc + while IFS='|' read -r name ver votes pop desc; do + [[ -z $name ]] && continue + aur_lines+=("$(aur_format_search_line "$name" "$ver" "$votes" "$pop" "$desc")") + done < <(aur_search "$query" || true) + fi + + if [[ ${MIAUR_BOTTOMUP:-0} -eq 1 ]]; then + local l + for l in "${aur_lines[@]}"; do printf '%b\n' "$l"; done + [[ ${#repo_lines[@]} -gt 0 && ${#aur_lines[@]} -gt 0 ]] && echo + local r + for r in "${repo_lines[@]}"; do printf '%s\n' "$r"; done + else + local r + for r in "${repo_lines[@]}"; do printf '%s\n' "$r"; done + [[ ${#repo_lines[@]} -gt 0 && ${#aur_lines[@]} -gt 0 ]] && echo + local l + for l in "${aur_lines[@]}"; do printf '%b\n' "$l"; done + fi +} + +# Interactive search + install (like `paru foo`) +cmd_interactive() { + local query="$*" + [[ -z $query ]] && die "specify a search query" + + local -a entries=() # type|name + local -a display=() + + # Repo results + if [[ ${MIAUR_MODE:-} != aur ]]; then + local line repo name rest + while IFS= read -r line; do + [[ $line =~ ^([a-z0-9_-]+)/([^[:space:]]+) ]] || continue + repo=${BASH_REMATCH[1]} + name=${BASH_REMATCH[2]} + entries+=("repo|$name") + display+=("${C_BOLD}${C_BLUE}${repo}/${name}${C_RESET}") + done < <(${MIAUR_PACMAN} -Ss "$query" 2>/dev/null | grep -E '^[a-z0-9_-]+/' || true) + fi + + # AUR results + if [[ ${MIAUR_MODE:-} != repo ]]; then + local name ver votes pop desc + while IFS='|' read -r name ver votes pop desc; do + [[ -z $name ]] && continue + entries+=("aur|$name") + display+=("${C_BOLD}${C_CYAN}aur/${name}${C_RESET} ${ver} (${votes})") + done < <(aur_search "$query" || true) + fi + + [[ ${#entries[@]} -eq 0 ]] && die "no matches found: $query" + + local i + if [[ ${MIAUR_BOTTOMUP:-0} -eq 1 ]]; then + for ((i = ${#display[@]} - 1; i >= 0; i--)); do + printf '%s%3d%s %b\n' "${C_DIM}" "$((i + 1))" "${C_RESET}" "${display[$i]}" + done + else + for ((i = 0; i < ${#display[@]}; i++)); do + printf '%s%3d%s %b\n' "${C_DIM}" "$((i + 1))" "${C_RESET}" "${display[$i]}" + done + fi + + printf '%s::%s Select packages (for example: 1 3; leave empty to cancel): ' \ + "${C_BOLD}${C_BLUE}" "${C_RESET}" + local sel= + read -r sel || exit 1 + [[ -z $sel ]] && { msg "Cancelled"; return 0; } + + local -a repo_pkgs=() aur_pkgs=() + local tok + for tok in $sel; do + if [[ $tok =~ ^[0-9]+$ ]]; then + local idx=$((tok - 1)) + ((idx >= 0 && idx < ${#entries[@]})) || die "invalid number: $tok" + local e=${entries[$idx]} + case "$e" in + repo\|*) repo_pkgs+=("${e#repo|}") ;; + aur\|*) aur_pkgs+=("${e#aur|}") ;; + esac + else + die "invalid selection: $tok" + fi + done + + if [[ ${#repo_pkgs[@]} -gt 0 ]]; then + as_root ${MIAUR_PACMAN} -S --needed --noconfirm "${repo_pkgs[@]}" + fi + if [[ ${#aur_pkgs[@]} -gt 0 ]]; then + install_aur_packages "${aur_pkgs[@]}" + fi +} + +cmd_info() { + local -a pkgs=("$@") + [[ ${#pkgs[@]} -eq 0 ]] && die "specify at least one package" + local p found + for p in "${pkgs[@]}"; do + found=0 + if [[ ${MIAUR_MODE:-} != aur ]] && ${MIAUR_PACMAN} -Si "$p" 2>/dev/null; then + found=1 + fi + if [[ ${MIAUR_MODE:-} != repo ]]; then + if aur_print_info "$p" 2>/dev/null; then + found=1 + fi + fi + [[ $found -eq 1 ]] || error "package not found: $p" + done +} + +cmd_sync_install() { + local -a pkgs=("$@") + [[ ${#pkgs[@]} -eq 0 ]] && die "specify at least one package" + + local -a repo_pkgs=() aur_pkgs=() + local p + for p in "${pkgs[@]}"; do + if [[ ${MIAUR_MODE:-} == aur ]]; then + aur_pkgs+=("$p") + elif [[ ${MIAUR_MODE:-} == repo ]]; then + repo_pkgs+=("$p") + elif repo_has_pkg "$p"; then + repo_pkgs+=("$p") + elif aur_exists "$p"; then + aur_pkgs+=("$p") + else + die "package not found: $p" + fi + done + + if [[ ${#repo_pkgs[@]} -gt 0 ]]; then + as_root ${MIAUR_PACMAN} -S --needed --noconfirm "${repo_pkgs[@]}" + fi + if [[ ${#aur_pkgs[@]} -gt 0 ]]; then + install_aur_packages "${aur_pkgs[@]}" + fi +} + +cmd_getpkgbuild() { + local -a pkgs=("$@") + [[ ${#pkgs[@]} -eq 0 ]] && die "specify at least one package" + local p dir + for p in "${pkgs[@]}"; do + if [[ $OP_PRINT -eq 1 ]]; then + dir=$(aur_clone "$p") + cat "$dir/PKGBUILD" + else + dir=$(aur_clone "$p") + # Copy to cwd like paru -G + if [[ -e ./$p ]]; then + warn "./$p already exists; replacing it" + rm -rf "./$p" + fi + cp -a "$dir" "./$p" + msg "Downloaded to ./$p" + fi + done +} + +cmd_clean() { + # -Sc: clean pacman cache + AUR clone cache + if [[ ${MIAUR_MODE:-} != aur ]]; then + as_root ${MIAUR_PACMAN} -Sc --noconfirm || true + fi + aur_clean_cache +} + +dispatch() { + # Full passthrough ops (R, D, F, T, U, or unknown) + if [[ $DO_PASSTHRU -eq 1 && $OP != S && $OP != Q ]]; then + # Rebuild approximate pacman command from original? Simpler: use saved extras + local -a args=() + case "$OP" in + R) args+=(-R) ;; + *) + # Fall back: user should use pacman; try to forward + args+=(-"${OP}") + ;; + esac + local c + for c in "${PACMAN_EXTRA[@]}"; do + args+=("-$c") + done + args+=("${TARGETS[@]}") + pacman_passthru "${args[@]}" + return $? + fi + + case "$OP" in + interactive) + need_cmd curl jq + cmd_interactive "${TARGETS[@]}" + ;; + S) + if [[ $OP_CLEAN -eq 1 ]]; then + cmd_clean + return + fi + if [[ $OP_SEARCH -gt 0 ]]; then + need_cmd curl jq + cmd_search "${TARGETS[@]}" + return + fi + if [[ $OP_INFO -eq 1 ]]; then + need_cmd curl jq + cmd_info "${TARGETS[@]}" + return + fi + if [[ $OP_UPGRADE -eq 1 ]]; then + need_cmd curl jq + if [[ $OP_AUR_ONLY -eq 1 ]]; then + # -Sua or -Su --aur + [[ $OP_REFRESH -eq 1 ]] && as_root ${MIAUR_PACMAN} -Sy --noconfirm + cmd_upgrade_aur + else + cmd_upgrade_all "$OP_REFRESH" + fi + return + fi + if [[ $OP_REFRESH -eq 1 && ${#TARGETS[@]} -eq 0 ]]; then + as_root ${MIAUR_PACMAN} -Sy --noconfirm + return + fi + # Install packages; optionally refresh first + [[ $OP_REFRESH -eq 1 ]] && as_root ${MIAUR_PACMAN} -Sy --noconfirm + need_cmd curl jq + cmd_sync_install "${TARGETS[@]}" + ;; + Q) + if [[ $OP_UPGRADE -eq 1 && $OP_AUR_ONLY -eq 1 ]]; then + need_cmd curl jq + cmd_query_aur_upgrades + return + fi + if [[ $OP_UPGRADE -eq 1 ]]; then + # -Qu: repo + AUR + ${MIAUR_PACMAN} -Qu || true + need_cmd curl jq + cmd_query_aur_upgrades || true + return + fi + # Default query passthrough + local -a qargs=(-Q) + local c + for c in "${PACMAN_EXTRA[@]}"; do + qargs+=("-$c") + done + [[ $OP_AUR_ONLY -eq 1 ]] && qargs+=(-m) + [[ $OP_SEARCH -gt 0 ]] && qargs+=(-s) + [[ $OP_INFO -eq 1 ]] && qargs+=(-i) + qargs+=("${TARGETS[@]}") + ${MIAUR_PACMAN} "${qargs[@]}" + ;; + G) + need_cmd curl jq git + cmd_getpkgbuild "${TARGETS[@]}" + ;; + B) + [[ ${#TARGETS[@]} -eq 0 ]] && TARGETS=(.) + need_cmd git makepkg + build_local_dirs "$OP_BUILD_INSTALL" "${TARGETS[@]}" + ;; + "") + if [[ $DO_PASSTHRU -eq 1 ]]; then + die "unable to parse operation; use pacman directly or see --help" + fi + die "no operation specified" + ;; + *) + die "unsupported operation: $OP" + ;; + esac +} diff --git a/lib/review.sh b/lib/review.sh new file mode 100644 index 0000000..67b8df2 --- /dev/null +++ b/lib/review.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# PKGBUILD review + +# Review PKGBUILD in dir. Returns 0 to continue, 1 to abort. +review_pkgbuild() { + local dir=$1 + local name=${2:-$(basename "$dir")} + + [[ ${MIAUR_SKIPREVIEW:-0} -eq 1 ]] && return 0 + + local pb="$dir/PKGBUILD" + [[ -f $pb ]] || die "PKGBUILD not found: $dir" + + msg "Reviewing the PKGBUILD for $name" + if command -v "${MIAUR_BAT}" >/dev/null 2>&1; then + "${MIAUR_BAT}" --style=plain --paging=always --language=bash "$pb" || true + elif command -v less >/dev/null 2>&1; then + less -R "$pb" || true + else + cat "$pb" + fi + + # Also show install script / other files briefly + local f + for f in "$dir"/*.install "$dir"/PKGBUILD; do + [[ -f $f ]] || continue + done + + while true; do + printf '%s::%s View diff (d), edit (e), continue (y), skip (s), or quit (q)? [Y/d/e/s/q] ' \ + "${C_BOLD}${C_BLUE}" "${C_RESET}" + local ans= + read -r ans || return 1 + ans=${ans:-y} + case "${ans,,}" in + y | yes | '') return 0 ;; + q | quit | n | no) + warn "Cancelled by user" + return 1 + ;; + s | skip) return 2 ;; + e | edit) + "${MIAUR_EDITOR}" "$pb" + ;; + d | diff) + if [[ -d $dir/.git ]]; then + (cd "$dir" && ${MIAUR_GIT} diff -- PKGBUILD) || true + else + warn "No Git history is available for comparison" + fi + ;; + *) + warn "Invalid input" + ;; + esac + done +} diff --git a/lib/upgrade.sh b/lib/upgrade.sh new file mode 100644 index 0000000..aa4dc39 --- /dev/null +++ b/lib/upgrade.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# AUR upgrade detection and application + +# List foreign packages (AUR candidates): name version +foreign_packages() { + pacman -Qm +} + +# Compare installed foreign pkgs with AUR; print upgradable: name local -> remote +aur_outdated() { + local -a names=() + local -A localver=() + local name ver + while read -r name ver; do + [[ -z $name ]] && continue + names+=("$name") + localver[$name]=$ver + done < <(foreign_packages) + + [[ ${#names[@]} -eq 0 ]] && return 0 + + # Batch in chunks of 100 + local i chunk=100 + local -a batch=() + for ((i = 0; i < ${#names[@]}; i++)); do + batch+=("${names[$i]}") + if ((${#batch[@]} == chunk || i == ${#names[@]} - 1)); then + local json + json=$(aur_multiinfo_json "${batch[@]}") || { + warn "Failed to query AUR updates" + batch=() + continue + } + local line rname rver + while IFS= read -r line; do + rname=${line%%|*} + rver=${line#*|} + [[ -z $rname ]] && continue + local lv=${localver[$rname]:-} + [[ -z $lv ]] && continue + if [[ $(vercmp_safe "$rver" "$lv") -gt 0 ]]; then + printf '%s %s -> %s\n' "$rname" "$lv" "$rver" + fi + done < <(jq -r '.[] | "\(.Name)|\(.Version)"' <<<"$json") + batch=() + fi + done +} + +# Print outdated like pacman -Qu +cmd_query_aur_upgrades() { + local name old _arrow new + while read -r name old _arrow new; do + [[ -z $name ]] && continue + printf '%s %s -> %s\n' "$name" "$old" "$new" + done < <(aur_outdated) + return 0 +} + +# Upgrade all outdated AUR packages +cmd_upgrade_aur() { + local -a pkgs=() + local name old arrow new + while read -r name old arrow new; do + [[ -z $name ]] && continue + pkgs+=("$name") + msg2 "$name ${old} -> ${new}" + done < <(aur_outdated) + + if [[ ${#pkgs[@]} -eq 0 ]]; then + msg "No AUR package upgrades are available" + return 0 + fi + msg "Upgrading ${#pkgs[@]} AUR package(s)" + install_aur_packages "${pkgs[@]}" +} + +# Full -Syu: refresh + repo upgrade + AUR upgrade +cmd_upgrade_all() { + local refresh=${1:-0} + sudoloop_start + local -a pac_args=() + [[ $refresh -eq 1 ]] && pac_args+=(-y) + pac_args+=(-Su) + + msg "Synchronizing databases and upgrading repository packages..." + # Use -Syu style via pacman + if [[ $refresh -eq 1 ]]; then + as_root ${MIAUR_PACMAN} -Syu --noconfirm || die "repository upgrade failed" + else + as_root ${MIAUR_PACMAN} -Su --noconfirm || die "repository upgrade failed" + fi + + msg "Checking for AUR updates..." + cmd_upgrade_aur + sudoloop_stop +} diff --git a/miaur b/miaur new file mode 100644 index 0000000..fc1e329 --- /dev/null +++ b/miaur @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail + +# miaur — pure-shell AUR helper (paru-like daily workflow) + +MIAUR_HOME=$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd) + +# shellcheck source=lib/common.sh +source "$MIAUR_HOME/lib/common.sh" +# shellcheck source=lib/config.sh +source "$MIAUR_HOME/lib/config.sh" +# shellcheck source=lib/aur_rpc.sh +source "$MIAUR_HOME/lib/aur_rpc.sh" +# shellcheck source=lib/clone.sh +source "$MIAUR_HOME/lib/clone.sh" +# shellcheck source=lib/review.sh +source "$MIAUR_HOME/lib/review.sh" +# shellcheck source=lib/deps.sh +source "$MIAUR_HOME/lib/deps.sh" +# shellcheck source=lib/build.sh +source "$MIAUR_HOME/lib/build.sh" +# shellcheck source=lib/upgrade.sh +source "$MIAUR_HOME/lib/upgrade.sh" +# shellcheck source=lib/cli.sh +source "$MIAUR_HOME/lib/cli.sh" +# shellcheck source=lib/dispatch.sh +source "$MIAUR_HOME/lib/dispatch.sh" + +main() { + miaur_load_config + + # Fast path: operations that are pure pacman wrappers with no AUR flags + # Keep original argv for -R/-D/-F/-T/-U when no miaur-specific long opts mixed in awkwardly. + # We still parse to detect them. + local -a orig=("$@") + parse_args "$@" + + # Special: if DO_PASSTHRU and OP empty, forward entire original command + if [[ $DO_PASSTHRU -eq 1 && -z ${OP:-} ]]; then + # Replace program name conceptually — strip miaur-only long options already consumed + as_root ${MIAUR_PACMAN} "${orig[@]}" + return $? + fi + + # For -R with extras like -Rns: OP=R DO_PASSTHRU=1 + if [[ ${OP:-} == R ]]; then + local -a rargs=(-R) + local c + for c in "${PACMAN_EXTRA[@]}"; do + rargs+=("-$c") + done + rargs+=("${TARGETS[@]}") + as_root ${MIAUR_PACMAN} "${rargs[@]}" + return $? + fi + + dispatch +} + +main "$@"