#!/bin/bash # mkhint - Manage hint files for slackrepo scripts # # Usage: # ./mkhint --version VERSION --hintfile FILE Update existing hint file # ./mkhint --version VERSION --new FILE Create new hint file # ./mkhint --new FILE Create new hint file (no version) # ./mkhint --hintfile FILE Update hint, suggest latest version via nvchecker # ./mkhint --check [FILE...] Check all (or named) hints for upstream updates # ./mkhint --list List hint files # ./mkhint --clean Remove .bak files from HINT_DIR # ./mkhint --no-dl --hintfile FILE Update hint, skip downloads, add NODOWNLOAD=yes # ./mkhint --no-dl --new FILE Create hint with NODOWNLOAD=yes # ./mkhint --help Show this help set -e # Default configuration REPO_DIR="/var/lib/sbopkg/SBo-danix/" HINT_DIR="/etc/slackrepo/SBo-danix/hintfiles/" TMP_DIR="/tmp/mkhint" NVCHECKER_CONFIG="$HOME/.config/nvchecker/nvchecker.toml" # create the temp dir if not existing if [[ ! -d $TMP_DIR ]]; then mkdir $TMP_DIR fi # Variables VERSION="" HINT_FILE="" NEW_HINT_FILE="" DELETE_HINT_FILES=() COMMAND="" NO_DL=0 # Show help message show_help() { cat <&2 exit 2 fi echo "Hint files in: $HINT_DIR" echo "=======================================================" printf "%-40s %10s %10s %-20s %s\n" "File" "HintVer" "SBOVer" "Category" "Created" echo "-------------------------------------------------------" local count=0 for file in "$HINT_DIR"/*.hint; do if [[ -f "$file" ]]; then local VER=$(grep "^VERSION" "$file" |cut -d '"' -f2) local name=$(basename "$file") local pkg="${name%.hint}" local info_file info_file=$(find "$REPO_DIR" -mindepth 2 -name "${pkg}.info" 2>/dev/null | head -1) local SBO_VER="" local category="" if [[ -f "$info_file" ]]; then SBO_VER=$(grep "^VERSION" "$info_file" | cut -d '"' -f2) category=$(basename "$(dirname "$(dirname "$info_file")")") fi local date=$(stat -c "%y" "$file" | cut -d'.' -f1) printf "%-40s %10s %10s %-20s %s\n" "$name" "$VER" "$SBO_VER" "$category" "$date" count=$((count + 1)) fi done if [[ $count -eq 0 ]]; then echo " (no hint files found)" fi echo "=======================================================" echo "Total: $count file(s)" } # Validate wget availability check_wget() { if ! command -v wget &> /dev/null; then echo "Error: wget is not installed. Please install wget first." >&2 exit 4 fi } # Validate nvchecker toolchain availability check_nvchecker() { local missing=() command -v nvchecker &> /dev/null || missing+=("nvchecker") command -v nvtake &> /dev/null || missing+=("nvtake") command -v jq &> /dev/null || missing+=("jq") if [[ ${#missing[@]} -gt 0 ]]; then echo "Error: required tool(s) not installed: ${missing[*]}" >&2 echo "Install nvchecker (provides nvchecker + nvtake) and jq." >&2 exit 4 fi if [[ ! -f "$NVCHECKER_CONFIG" ]]; then echo "Error: nvchecker config not found: $NVCHECKER_CONFIG" >&2 exit 2 fi } # Echo the newver-keyfile path declared in [__config__] of NVCHECKER_CONFIG _nvchecker_newver_path() { # Grab the `newver = "..."` value; tolerate spaces around = local line line=$(grep -E '^[[:space:]]*newver[[:space:]]*=' "$NVCHECKER_CONFIG" | head -1) [[ -z "$line" ]] && return 1 # extract the quoted path local path path=$(printf '%s\n' "$line" | sed -E 's/^[^"]*"([^"]*)".*/\1/') [[ -z "$path" ]] && return 1 # expand a leading ~ to $HOME path="${path/#\~/$HOME}" printf '%s\n' "$path" } # Echo the latest version nvchecker found for a package, or return non-zero # Usage: latest=$(nvchecker_latest pkg) || handle "no version" nvchecker_latest() { local pkg="$1" local keyfile keyfile=$(_nvchecker_newver_path) || return 1 [[ -f "$keyfile" ]] || return 1 local ver ver=$(jq -r --arg p "$pkg" '.data[$p].version // empty' "$keyfile" 2>/dev/null) [[ -z "$ver" ]] && return 1 printf '%s\n' "$ver" } # download files download_file() { local url="$1" local dlfile="${TMP_DIR}/download" if [[ -f $dlfile ]]; then rm $dlfile fi # Download the file if [[ ! -z $1 ]]; then wget -O "$dlfile" "$url" || return 1 fi # calculate md5 local md5=$(md5sum "$dlfile" | awk '{print $1}') rm $dlfile echo $md5 } # Create new hint file create_new_hint_file() { cd $HINT_DIR local file="$1" local normalized_file="${file}" if [[ "$file" != *.hint ]]; then normalized_file="${file}.hint" fi # search repository for .info file info=$(find $REPO_DIR -mindepth 2 -name ${normalized_file%.hint}.info) # Check if file exists if [[ ! -f "$normalized_file" ]]; then # the hint file we want to create doesn't exists, so we can check # the sbo repository for a .info file and use that as hint if [[ -n $info ]]; then cp $info $normalized_file # remove unwanted lines from hint file sed -i -e "/^PRGNAM=/d" \ -e "/^HOMEPAGE=/d" \ -e "/^MAINTAINER=/d" \ -e "/^EMAIL=/d" \ -e "s/^REQUIRES=/#REQUIRES=/" \ "${normalized_file}" if grep -q '^ARCH=' $normalized_file; then sed -i 's/^ARCH=.*/ARCH="x86_64"/' $normalized_file else echo 'ARCH="x86_64"' >> $normalized_file fi if [[ -n "$VERSION" ]]; then local old_version old_version=$(grep '^VERSION=' "$normalized_file" | sed 's/VERSION="//;s/"$//') sed -i "s/${old_version}/${VERSION}/g" "$normalized_file" update_checksums "$normalized_file" fi if [[ $NO_DL -eq 1 ]]; then add_nodownload "$normalized_file" fi echo "generated $normalized_file from $(basename $info)." echo "Check variables before using." add_nvchecker_section "${normalized_file%.hint}" "$info" fi else echo "Hint file exists: $normalized_file" >&2 mv "$normalized_file" "${normalized_file}.bak" echo "Backed up to: ${normalized_file}.bak" >&2 # Create new hint file with empty variables cat > "$normalized_file" <> "$NVCHECKER_CONFIG" echo "nvchecker: review/fill [${pkg}] section in $NVCHECKER_CONFIG" } # Add NODOWNLOAD=yes after MD5SUM_x86_64 line if not already present add_nodownload() { local file="$1" if ! grep -q '^NODOWNLOAD=' "$file"; then sed -i '/^MD5SUM_x86_64=/a NODOWNLOAD=yes' "$file" fi } # Parse multiline variable value (handles \ continuation lines) # Prints each whitespace-separated token on its own line parse_multiline_var() { local varname="$1" local file="$2" # Join continuation lines, strip variable name and quotes, print one token per line awk -v var="${varname}" ' BEGIN { found=0; buf="" } !found && $0 ~ "^"var"=\"" { found=1 buf=$0 sub("^"var"=\"", "", buf) if (buf !~ /\\[[:space:]]*$/) { gsub(/"[[:space:]]*$/, "", buf) n=split(buf, arr, /[[:space:]]+/) for (k=1;k<=n;k++) if(arr[k]!="") print arr[k] found=0; buf="" } else { gsub(/\\[[:space:]]*$/, "", buf) } next } found { if ($0 ~ /\\[[:space:]]*$/) { line=$0; gsub(/\\[[:space:]]*$/, "", line) buf=buf" "line } else { line=$0; gsub(/"[[:space:]]*$/, "", line) buf=buf" "line n=split(buf, arr, /[[:space:]]+/) for (k=1;k<=n;k++) if(arr[k]!="") print arr[k] found=0; buf="" } } ' "$file" } # Prompt user for updated continuation URLs; returns updated URLs via nameref array # First URL is always kept as-is (already updated by version sed before this call) prompt_continuation_urls() { local -n _urls="$1" # nameref: array of current URLs local varname="$2" local i for (( i=1; i<${#_urls[@]}; i++ )); do local current="${_urls[$i]}" echo "" echo " ${varname} line $((i+1)) (current): $current" read -r -p " New URL (leave blank to keep): " new_url if [[ -n "$new_url" ]]; then _urls[$i]="$new_url" fi done } # Build multiline variable string for writing back to file # Usage: build_multiline_value urls_array -> prints quoted multiline value build_multiline_value() { local -n _arr="$1" local count=${#_arr[@]} local i for (( i=0; i&2 || true local latest latest=$(nvchecker_latest "$pkg") || { echo "Error: no nvchecker result for '$pkg'. Add/fix its [${pkg}] section in $NVCHECKER_CONFIG" >&2 return 1 } # Read current version from the hint file (best effort, for display) local hintpath="${HINT_DIR%/}/${pkg}.hint" local current="" [[ -f "$hintpath" ]] && current=$(grep '^VERSION=' "$hintpath" | sed 's/VERSION="//;s/"$//') local answer read -r -p "current ${current:-?}, latest ${latest}. Use ${latest}? [Y/n] (or type a version) " answer >&2 answer="${answer:-Y}" case "$answer" in [Yy]) printf '%s\n' "$latest" ;; [Nn]) return 1 ;; *) printf '%s\n' "$answer" ;; esac } # Download files and update MD5SUM/MD5SUM_x86_64 in hint file update_checksums() { local file="$1" _process_download_var "DOWNLOAD" "MD5SUM" "$file" _process_download_var "DOWNLOAD_x86_64" "MD5SUM_x86_64" "$file" } # Process one DOWNLOAD/MD5SUM variable pair in a hint file _process_download_var() { local dl_var="$1" local md5_var="$2" local file="$3" # Read current URLs into array mapfile -t urls < <(parse_multiline_var "$dl_var" "$file") [[ ${#urls[@]} -eq 0 ]] && return [[ "${urls[0]}" == "UNSUPPORTED" || "${urls[0]}" == "UNTESTED" ]] && return # Read current md5sums into array (parallel to urls) mapfile -t md5s < <(parse_multiline_var "$md5_var" "$file") # Save original URLs for change detection after prompt local orig_urls=("${urls[@]}") # Prompt user to update continuation URLs if present if (( ${#urls[@]} > 1 )); then echo "" echo "Multiline ${dl_var} detected in $(basename "$file")." prompt_continuation_urls urls "$dl_var" fi # Download and calculate md5 for each URL local new_md5s=() local i for (( i=0; i<${#urls[@]}; i++ )); do local url="${urls[$i]}" if (( i == 0 )); then # Always re-download first URL echo "Downloading: $url" new_md5s+=( "$(download_file "$url")" ) else # Only re-download if URL changed from original if [[ "$url" != "${orig_urls[$i]}" ]]; then echo "Downloading (updated): $url" new_md5s+=( "$(download_file "$url")" ) else echo "Keeping existing md5 for: $url" new_md5s+=( "${md5s[$i]}" ) fi fi done # Rebuild and write back DOWNLOAD variable (may have updated continuation URLs) local new_dl_value new_dl_value=$(build_multiline_value urls) # Strip surrounding quotes — perl wraps them in the substitution new_dl_value="${new_dl_value#\"}" new_dl_value="${new_dl_value%\"}" perl -i -0pe 'BEGIN{$v=shift} s|^'"${dl_var}"'="[^"]*(?:\\\n[^"]*)*"|'"${dl_var}"'="$v"|m' \ "$new_dl_value" "$file" # Rebuild and write back MD5SUM variable local new_md5_value new_md5_value=$(build_multiline_value new_md5s) new_md5_value="${new_md5_value#\"}" new_md5_value="${new_md5_value%\"}" perl -i -0pe 'BEGIN{$v=shift} s|^'"${md5_var}"'="[^"]*(?:\\\n[^"]*)*"|'"${md5_var}"'="$v"|m' \ "$new_md5_value" "$file" } # Update existing hint file update_hint_file() { cd "$HINT_DIR" local file="$1" local new_version="$2" local old_version="" local normalized_file="${file}" if [[ "$file" != *.hint ]]; then normalized_file="${file}.hint" fi # Check if file exists if [[ ! -f "$normalized_file" ]]; then echo "Error: Hint file does not exist: $normalized_file" >&2 exit 2 fi # Force backup as precaution before modifying echo "Hint file exists: $normalized_file" >&2 cp "$normalized_file" "${normalized_file}.bak" echo "Backed up to: ${normalized_file}.bak" >&2 # Extract current version from hint file old_version=$(grep '^VERSION=' "$normalized_file" | sed 's/VERSION="//;s/"$//') # Use sed for global replacement of OLD_VERSION in all variables sed -i "s/${old_version}/${new_version}/g" "$normalized_file" update_checksums "$normalized_file" if [[ $NO_DL -eq 1 ]]; then add_nodownload "$normalized_file" fi hf=$(cat $normalized_file) echo "Updated hint file: $normalized_file" echo "==========================================" echo -n "$hf" echo; echo "==========================================" } # Prompt to run slackrepo update after a hint file update prompt_slackrepo() { local pkg="$1" local answer read -r -p "Run 'slackrepo update $pkg'? [Y/n] " answer answer="${answer:-Y}" if [[ "$answer" =~ ^[Yy]$ ]]; then slackrepo update "$pkg" fi } # Delete a hint file (and .bak if present) delete_hint_file() { local file="$1" local normalized_file="${file}" if [[ "$file" != *.hint ]]; then normalized_file="${file}.hint" fi local full_path="${HINT_DIR}/${normalized_file}" if [[ ! -f "$full_path" ]]; then echo "Error: Hint file does not exist: $full_path" >&2 exit 2 fi rm "$full_path" echo "Deleted: $full_path" local bak_path="${full_path}.bak" if [[ -f "$bak_path" ]]; then rm "$bak_path" echo "Deleted: $bak_path" fi } # Clean .bak files from HINT_DIR clean_bak_files() { if [[ ! -d "$HINT_DIR" ]]; then echo "Error: Hint directory does not exist: $HINT_DIR" >&2 exit 2 fi local count=0 for bakfile in "$HINT_DIR"/*.bak; do if [[ -f "$bakfile" ]]; then rm "$bakfile" count=$((count + 1)) fi done echo "Removed $count .bak file(s) from $HINT_DIR" } # Bulk-check hint files for upstream updates and apply interactively. # Usage: check_updates [pkg...] (no args = all *.hint in HINT_DIR) check_updates() { check_nvchecker if [[ ! -d "$HINT_DIR" ]]; then echo "Error: Hint directory does not exist: $HINT_DIR" >&2 exit 2 fi # Build the target package list local targets=() if [[ $# -gt 0 ]]; then targets=("$@") else local f for f in "$HINT_DIR"/*.hint; do [[ -f "$f" ]] || continue local b; b=$(basename "$f"); targets+=("${b%.hint}") done fi # Refresh nvchecker results once for everything echo "Running nvchecker..." nvchecker -c "$NVCHECKER_CONFIG" >&2 || true # Classify each target local outdated_pkgs=() outdated_old=() outdated_new=() outdated_flag=() local pkg for pkg in "${targets[@]}"; do local hintpath="${HINT_DIR%/}/${pkg}.hint" [[ -f "$hintpath" ]] || { echo "skip ${pkg}: no hint file"; continue; } local current; current=$(grep '^VERSION=' "$hintpath" | sed 's/VERSION="//;s/"$//') local latest latest=$(nvchecker_latest "$pkg") || { echo "skip ${pkg}: no nvchecker source"; continue; } [[ "$current" == "$latest" ]] && continue # up to date # determine direction with sort -V local newest; newest=$(printf '%s\n%s\n' "$current" "$latest" | sort -V | tail -1) local flag="update" [[ "$newest" == "$current" ]] && flag="?downgrade" outdated_pkgs+=("$pkg") outdated_old+=("$current") outdated_new+=("$latest") outdated_flag+=("$flag") done if [[ ${#outdated_pkgs[@]} -eq 0 ]]; then echo "all up to date" return 0 fi # Report echo "" echo "Updates available:" local i for (( i=0; i<${#outdated_pkgs[@]}; i++ )); do local note=""; [[ "${outdated_flag[$i]}" == "?downgrade" ]] && note=" (?downgrade)" printf " %-30s %s -> %s%s\n" "${outdated_pkgs[$i]}" "${outdated_old[$i]}" "${outdated_new[$i]}" "$note" done echo "" # Per-package confirm + update local updated=() for (( i=0; i<${#outdated_pkgs[@]}; i++ )); do local p="${outdated_pkgs[$i]}" local note=""; [[ "${outdated_flag[$i]}" == "?downgrade" ]] && note=" (?downgrade)" local answer read -r -p "${p} ${outdated_old[$i]} -> ${outdated_new[$i]}${note}. Update? [Y/n] " answer answer="${answer:-Y}" if [[ "$answer" =~ ^[Yy]$ ]]; then update_hint_file "$p" "${outdated_new[$i]}" nvtake -c "$NVCHECKER_CONFIG" "$p" >&2 || true updated+=("$p") fi done # Single slackrepo prompt for everything updated if [[ ${#updated[@]} -gt 0 ]]; then local answer read -r -p "Run 'slackrepo update ${updated[*]}'? [Y/n] " answer answer="${answer:-Y}" if [[ "$answer" =~ ^[Yy]$ ]]; then slackrepo update "${updated[@]}" fi fi } # Main function main() { local parsed parsed=$(getopt -o v:f:n:lcCdNh \ --long version:,hintfile:,new:,list,clean,check,delete,no-dl,help \ -n 'mkhint' -- "$@") || { show_help; exit 1; } eval set -- "$parsed" while true; do case "$1" in --version|-v) VERSION="$2" shift 2 ;; --hintfile|-f) HINT_FILE="$2" shift 2 ;; --new|-n) NEW_HINT_FILE="$2" shift 2 ;; --list|-l) COMMAND="list" shift ;; --clean|-c) COMMAND="clean" shift ;; --check|-C) COMMAND="check" shift ;; --delete|-d) COMMAND="delete" shift ;; --no-dl|-N) NO_DL=1 shift ;; --help|-h) COMMAND="help" shift ;; --) shift break ;; *) echo "Unknown option: $1" >&2 show_help exit 1 ;; esac done # Collect remaining positional args as delete targets while [[ $# -gt 0 ]]; do DELETE_HINT_FILES+=("$1") shift done if [[ -z "$COMMAND" ]]; then # Default to update hint file if VERSION and HINT_FILE are provided if [[ -n "$HINT_FILE" ]]; then COMMAND="update" elif [[ -n "$NEW_HINT_FILE" ]]; then COMMAND="new" elif [[ ${#DELETE_HINT_FILES[@]} -gt 0 ]]; then COMMAND="delete" fi fi if [[ $NO_DL -eq 1 && -z "$HINT_FILE" && -z "$NEW_HINT_FILE" ]]; then echo "Error: --no-dl requires --hintfile or --new" >&2 exit 1 fi if [[ "$COMMAND" == "check" && ( -n "$VERSION" || -n "$HINT_FILE" || -n "$NEW_HINT_FILE" ) ]]; then echo "Error: --check cannot be combined with --version/--hintfile/--new" >&2 exit 1 fi case "$COMMAND" in help) show_help ;; list) list_hint_files ;; clean) clean_bak_files ;; check) check_updates "${DELETE_HINT_FILES[@]}" ;; update) check_wget if [[ -z "$VERSION" ]]; then check_nvchecker VERSION=$(suggest_version "$HINT_FILE") || { echo "Aborted." >&2; exit 0; } check_nvchecker_take=1 fi update_hint_file "$HINT_FILE" "$VERSION" if [[ "${check_nvchecker_take:-0}" -eq 1 ]]; then nvtake -c "$NVCHECKER_CONFIG" "$HINT_FILE" >&2 || true fi prompt_slackrepo "$HINT_FILE" ;; new) if [[ -n "$VERSION" ]]; then check_wget fi create_new_hint_file "$NEW_HINT_FILE" ;; delete) for f in "${DELETE_HINT_FILES[@]}"; do delete_hint_file "$f" done ;; *) echo "Error: Unknown command: $COMMAND" >&2 show_help exit 1 ;; esac } main "$@"