#!/bin/bash # mkhint - Manage hint files for slackrepo scripts # # Usage: # ./mkhint --set-version VERSION --hintfile FILE Update existing hint file # ./mkhint --set-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" # Deps that exist on Slackware stable but are unneeded on -current (system # package or newer already present). Listed one per line, '#' comments allowed. # Missing file = empty list = phantom-dep handling is a no-op. PHANTOM_DEPS_FILE="$HOME/.config/mkhint/phantom-deps" # Packages whose extra DOWNLOAD lines are driven by an upstream deps manifest # (e.g. neovim's cmake.deps/deps.txt). One entry per line: # # Missing file = empty list = feature is a no-op. BUNDLE_MANIFEST_FILE="$HOME/.config/mkhint/bundle-manifests" # Built-package repository (slackrepo output tree: //-*.txz). PACKAGES_DIR="/repo/" # ponytail: sourced config, defaults above win when file absent. A syntax-broken # config aborts under `set -e` — acceptable for a single-user tool. MKHINT_CONFIG="$HOME/.config/mkhint/config" [[ -f "$MKHINT_CONFIG" ]] && source "$MKHINT_CONFIG" # create the temp dir if not existing if [[ ! -d $TMP_DIR ]]; then mkdir $TMP_DIR fi readonly MKHINT_VERSION="1.2.0" # Variables VERSION="" HINT_FILE="" NEW_HINT_FILE="" DELETE_HINT_FILES=() MATCHED_PKGS=() REVIEW_PKGS=() LIST_PKGS=() SHOW_LIST="" RUN_REVIEW="" COMMAND="" NO_DL=0 FORCE=0 # Show help message show_help() { cat <&2 exit 2 fi # Color only on a TTY, or when forced for tests. tput optional. local c_on="" c_off="" g_on="" if [[ -n "$MKHINT_FORCE_COLOR" || -t 1 ]]; then if command -v tput &>/dev/null && tput setaf 3 &>/dev/null; then c_on=$(tput setaf 3); g_on=$(tput setaf 2); c_off=$(tput sgr0) else c_on=$'\033[33m'; g_on=$'\033[32m'; c_off=$'\033[0m' fi fi echo "Hint files in: $HINT_DIR" echo "=======================================================" printf "%-40s %22s %22s %-20s %-6s %s\n" "File" "HintVer" "SBOVer" "Category" "DelReq" "Created" echo "-------------------------------------------------------" MATCHED_PKGS=() local count=0 matched=0 for file in "$HINT_DIR"/*.hint; do if [[ -f "$file" ]]; then local VER; VER=$(grep "^VERSION" "$file" |cut -d '"' -f2) # Skip hints with no VERSION set. [[ -z "$VER" ]] && continue local delreq="" grep -q '^DELREQUIRES="..*"' "$file" && delreq="✓" local name; 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; date=$(stat -c "%y" "$file" | cut -d'.' -f1) local dr; dr=$(_pad_glyph "$delreq" 6) local hv sv hv=$(printf "%22s" "$VER") sv=$(printf "%22s" "$SBO_VER") if [[ -n "$SBO_VER" && "$VER" == "$SBO_VER" ]]; then # equal → whole row yellow local row row=$(printf "%-40s %s %s %-20s %s %s" "$name" "$hv" "$sv" "$category" "$dr" "$date") printf "%s%s%s\n" "$c_on" "$row" "$c_off" MATCHED_PKGS+=("$pkg") matched=$((matched + 1)) else # differ (or no SBO_VER) → green the newer cell, row plain if [[ -n "$SBO_VER" ]]; then local newer newer=$(printf '%s\n%s\n' "$VER" "$SBO_VER" | sort -V | tail -1) if [[ "$newer" == "$VER" ]]; then hv="${g_on}${hv}${c_off}" else sv="${g_on}${sv}${c_off}" fi fi printf "%-40s %s %s %-20s %s %s\n" "$name" "$hv" "$sv" "$category" "$dr" "$date" fi count=$((count + 1)) fi done if [[ $count -eq 0 ]]; then echo " (no hint files found)" fi echo "=======================================================" echo "Total: $count file(s)" if [[ $matched -gt 0 && -n "$c_on" ]]; then echo "(yellow row = versions match; green = newer side)" fi } # Show hint side-by-side with its .info. Output to the given fd (default 1). _show_hint_diff() { local pkg="$1" local fd="${2:-1}" local hint="${HINT_DIR%/}/${pkg}.hint" local info info=$(find "$REPO_DIR" -mindepth 2 -name "${pkg}.info" 2>/dev/null | head -1) echo "" >&"$fd" echo "=== $pkg ===" >&"$fd" if command -v git &>/dev/null; then git diff --no-index --color=auto "$hint" "$info" >&"$fd" || true else diff -y --width="${COLUMNS:-160}" "$hint" "$info" >&"$fd" || true fi } # Diff one hint against its .info and prompt Keep/Delete/Skip. # Human-facing output goes to stderr; echoes only "deleted" or "kept" on stdout # so the caller can capture the result. _review_one_hint() { local pkg="$1" local hint="${HINT_DIR%/}/${pkg}.hint" _show_hint_diff "$pkg" 2 local ans read -r -p "Review $pkg: [K]eep / [D]elete / [S]kip (default Keep): " ans case "$ans" in [Dd]) _remove_hint "$hint" >&2 echo deleted ;; [Ss]|[Kk]|"") echo "Kept: $pkg" >&2 echo kept ;; *) echo "Unrecognised answer; keeping $pkg" >&2 echo kept ;; esac } # Review hints. With package args: review each named hint (any version), # validating existence first (exit 2 on a missing hint). With no args: review # MATCHED_PKGS (populated by list_hint_files). review_hint_files() { local -a pkgs if [[ $# -gt 0 ]]; then pkgs=("$@") local p for p in "${pkgs[@]}"; do if [[ ! -f "${HINT_DIR%/}/${p}.hint" ]]; then echo "Error: hint file not found: ${HINT_DIR%/}/${p}.hint" >&2 exit 2 fi done else if [[ ${#MATCHED_PKGS[@]} -eq 0 ]]; then echo "No hints match their SBo version; nothing to review." return 0 fi pkgs=("${MATCHED_PKGS[@]}") fi local deleted=0 kept=0 pkg result for pkg in "${pkgs[@]}"; do result=$(_review_one_hint "$pkg") case "$result" in deleted) deleted=$((deleted + 1)) ;; *) kept=$((kept + 1)) ;; esac done echo "" echo "Reviewed ${#pkgs[@]} hint(s): deleted $deleted, kept $kept." } # 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}" # nvchecker resolves a relative keyfile path against the config file's # directory (not the CWD), so do the same here. if [[ "$path" != /* ]]; then path="$(dirname "$NVCHECKER_CONFIG")/$path" fi printf '%s\n' "$path" } # Echo the latest version nvchecker found for a package, or return non-zero # Normalize an upstream version to our packaging convention: '-' is illegal in # a SlackBuild version (breaks PRGNAM parsing), so we store it as '_'. Compare # the normalized forms so e.g. upstream 2026-06-02 == packaged 2026_06_02. _normalize_version() { printf '%s\n' "${1//-/_}" } # 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; md5=$(md5sum "$dlfile" | awk '{print $1}') rm "$dlfile" echo "$md5" } # ── phantom-dep handling (deps unneeded on -current) ────────────────────────── # Load PHANTOM_DEPS_FILE into the PHANTOM_DEPS array (one dep per line, '#' # comments and blank lines ignored). Missing file => empty array. PHANTOM_DEPS=() load_phantom_deps() { PHANTOM_DEPS=() [[ -f "$PHANTOM_DEPS_FILE" ]] || return 0 local line while IFS= read -r line; do line="${line%%#*}" # strip comment line="${line//[[:space:]]/}" # strip whitespace [[ -n "$line" ]] && PHANTOM_DEPS+=("$line") done < "$PHANTOM_DEPS_FILE" } # Read REQUIRES="..." from an .info file and echo the phantom deps it contains, # space-separated. Empty output if none. phantom_deps_in_info() { local info="$1" requires dep [[ -f "$info" ]] || return 0 requires=$(. "$info" >/dev/null 2>&1; echo "${REQUIRES:-}") local hits=() for dep in "${PHANTOM_DEPS[@]}"; do [[ " $requires " == *" $dep "* ]] && hits+=("$dep") done echo "${hits[*]}" } # Merge a set of deps into a hint file's DELREQUIRES (union, dedup), preserving # all other content. Creates a minimal hint if the file is absent. Backs up an # existing file to .bak first (mkhint convention). Args: hintpath dep... merge_delrequires() { local hint="$1"; shift local new_deps=("$@") [[ ${#new_deps[@]} -eq 0 ]] && return 0 local existing="" combined if [[ -f "$hint" ]]; then existing=$(. "$hint" >/dev/null 2>&1; echo "${DELREQUIRES:-}") fi # union existing + new, dedup, first-seen order combined=$(printf '%s\n' $existing "${new_deps[@]}" | awk 'NF && !seen[$0]++' | tr '\n' ' ') combined="${combined% }" if [[ -f "$hint" ]]; then # no change needed? (all new deps already present) → skip, don't churn .bak local cur_norm new_norm cur_norm=$(printf '%s\n' $existing | awk 'NF && !seen[$0]++' | sort | tr '\n' ' ') new_norm=$(printf '%s\n' $existing "${new_deps[@]}" | awk 'NF && !seen[$0]++' | sort | tr '\n' ' ') [[ "$cur_norm" == "$new_norm" ]] && return 0 cp "$hint" "${hint}.bak" if grep -q '^DELREQUIRES=' "$hint"; then sed -i "s#^DELREQUIRES=.*#DELREQUIRES=\"$combined\"#" "$hint" else printf 'DELREQUIRES="%s"\n' "$combined" >> "$hint" fi else printf 'DELREQUIRES="%s"\n' "$combined" > "$hint" fi } # Bulk sweep: for every package in REPO_DIR whose REQUIRES contains a phantom # dep, ensure its hint carries the matching DELREQUIRES. Idempotent. fix_current() { load_phantom_deps if [[ ${#PHANTOM_DEPS[@]} -eq 0 ]]; then echo "No phantom deps configured ($PHANTOM_DEPS_FILE). Nothing to do." return 0 fi echo "Sweeping for phantom deps: ${PHANTOM_DEPS[*]}" local info prgnam deps count=0 while IFS= read -r info; do prgnam=$(basename "$info" .info) deps=$(phantom_deps_in_info "$info") [[ -z "$deps" ]] && continue merge_delrequires "$HINT_DIR/$prgnam.hint" $deps echo " $prgnam <- DELREQUIRES: $deps" (( count++ )) || true done < <(find "$REPO_DIR" -mindepth 2 -maxdepth 3 -name '*.info' | sort) echo "Done. $count package(s) with phantom deps." } # ── bundled-dep manifest handling ───────────────────────────────────────────── # Reconcile a bundle package's extra DOWNLOAD lines against an upstream deps # manifest (opt-in via BUNDLE_MANIFEST_FILE). See docs spec 2026-07-07. # parse_manifest — print one download URL per line for each NAME_URL # entry. SHA256 lines ignored (hints use MD5SUM; we re-download and md5). parse_manifest() { local file="$1" [[ -f "$file" ]] || return 0 awk '/_URL[[:space:]]/ { print $2 }' "$file" } # _url_stem — reduce a download URL's basename to a comparison stem: # drop a trailing archive extension, then a trailing - or the version # is the whole tail after the name. Version = [vV]?[0-9]... or a 7+ hex sha. _url_stem() { local base="${1##*/}" base="${base%.tar.gz}"; base="${base%.tar.xz}"; base="${base%.tar.bz2}" base="${base%.zip}"; base="${base%.tgz}" # strip a trailing - (version starts with optional v then a digit, # or is a 7+ char hex sha) if [[ "$base" =~ ^(.+)-[vV]?[0-9].*$ ]]; then base="${BASH_REMATCH[1]}" elif [[ "$base" =~ ^(.+)-[0-9a-f]{7,}$ ]]; then base="${BASH_REMATCH[1]}" fi printf '%s\n' "$base" } # _url_repo — echo owner/repo for a github URL, empty otherwise. _url_repo() { [[ "$1" =~ github\.com/([^/]+)/([^/]+) ]] && printf '%s/%s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]%.git}" } # match_dep_url — echo the manifest URL that # corresponds to hint_url, or nothing. Repo-path match first, then exact # basename-stem fallback (for blob hosts where the repo path is ambiguous). match_dep_url() { local hint_url="$1" list="$2" local hrepo; hrepo=$(_url_repo "$hint_url") local m mrepo # pass 1: repo-path (skip the shared-blob-host repo neovim/deps so a blob # URL never wins on repo path; it must match on stem instead) if [[ -n "$hrepo" && "$hrepo" != */deps ]]; then while IFS= read -r m; do [[ -z "$m" ]] && continue mrepo=$(_url_repo "$m") if [[ -n "$mrepo" && "$mrepo" != */deps && "$mrepo" == "$hrepo" ]]; then printf '%s\n' "$m"; return 0 fi done <<< "$list" fi # pass 2: exact stem local hstem; hstem=$(_url_stem "$hint_url") while IFS= read -r m; do [[ -z "$m" ]] && continue [[ "$(_url_stem "$m")" == "$hstem" ]] && { printf '%s\n' "$m"; return 0; } done <<< "$list" return 1 } # Loaded map: pkg name -> URL template. Populated by load_bundle_manifests. declare -A BUNDLE_MANIFESTS=() load_bundle_manifests() { BUNDLE_MANIFESTS=() [[ -f "$BUNDLE_MANIFEST_FILE" ]] || return 0 local line pkg url while IFS= read -r line; do line="${line%%#*}" [[ -z "${line// }" ]] && continue read -r pkg url <<< "$line" [[ -n "$pkg" && -n "$url" ]] && BUNDLE_MANIFESTS["$pkg"]="$url" done < "$BUNDLE_MANIFEST_FILE" } # manifest_url_for — echo the manifest URL for pkg with # {VERSION} substituted. Non-zero if pkg is not listed. manifest_url_for() { local pkg="$1" ver="$2" local tmpl="${BUNDLE_MANIFESTS[$pkg]:-}" [[ -z "$tmpl" ]] && return 1 printf '%s\n' "${tmpl//\{VERSION\}/$ver}" } # True if pkg has a bundle manifest configured. pkg_has_manifest() { [[ -n "${BUNDLE_MANIFESTS[$1]:-}" ]] } # fetch_manifest — download the manifest to a temp file, echo its path. # Non-zero on wget failure. Caller cleans up (path is under TMP_DIR). fetch_manifest() { local url="$1" [[ -d "$TMP_DIR" ]] || mkdir -p "$TMP_DIR" local out="${TMP_DIR}/manifest" rm -f "$out" wget -O "$out" "$url" >&2 || return 1 [[ -s "$out" ]] || return 1 printf '%s\n' "$out" } # reconcile_bundle_deps # mode = report (print only) | apply (rewrite matched changed lines + md5). # Reconciles the DOWNLOAD line's extra URLs (index >= 1) against the manifest. # The primary URL (index 0) is never touched here. Guarded for set -e. reconcile_bundle_deps() { local pkg="$1" hint="$2" murl="$3" mode="$4" # Display label for a dep URL: _url_stem is basename-only, so for a github # "archive/vX.Y.Z.tar.gz" tarball (no repo name in the filename) it just # returns the bare version — fall back to the repo name in that case. local _dep_label _dep_label() { local s; s=$(_url_stem "$1") if [[ "$s" =~ ^[vV]?[0-9] ]]; then local r; r=$(_url_repo "$1") [[ -n "$r" ]] && { printf '%s\n' "${r#*/}"; return; } fi printf '%s\n' "$s" } local mfile if ! mfile=$(fetch_manifest "$murl"); then echo " $pkg: manifest unavailable ($murl) — bundled deps left as-is (retry with --force)" return 0 fi local -a manifest_urls mapfile -t manifest_urls < <(parse_manifest "$mfile") rm -f "$mfile" if [[ ${#manifest_urls[@]} -eq 0 ]]; then echo " $pkg: manifest empty/unrecognized — bundled deps left as-is" return 0 fi local mlist; printf -v mlist '%s\n' "${manifest_urls[@]}" local -a urls mapfile -t urls < <(parse_multiline_var "DOWNLOAD" "$hint") (( ${#urls[@]} <= 1 )) && { echo " $pkg: no bundled deps in DOWNLOAD"; return 0; } local -a md5s mapfile -t md5s < <(parse_multiline_var "MD5SUM" "$hint") # Track which manifest URLs got matched (for the FYI of unmatched ones). local -A matched_manifest=() local -a new_urls=("${urls[@]}") local -a changed_idx=() changed_from=() changed_to=() local i m # The primary hint URL (index 0) is never reconciled, but its manifest # counterpart must count as "matched" so the FYI doesn't always flag it. local _pm; _pm=$(match_dep_url "${urls[0]}" "$mlist") || _pm="" [[ -n "$_pm" ]] && matched_manifest["$_pm"]=1 for (( i=1; i<${#urls[@]}; i++ )); do m=$(match_dep_url "${urls[$i]}" "$mlist") || m="" if [[ -z "$m" ]]; then echo " $pkg: $(_dep_label "${urls[$i]}") — no manifest match (left as-is)" continue fi matched_manifest["$m"]=1 if [[ "$m" != "${urls[$i]}" ]]; then changed_idx+=("$i"); changed_from+=("${urls[$i]}"); changed_to+=("$m") new_urls[$i]="$m" fi done # Manifest-only deps FYI (manifest URLs never matched by any hint line). local -a extra=() for m in "${manifest_urls[@]}"; do [[ -z "${matched_manifest[$m]:-}" ]] && extra+=("$(_dep_label "$m")") done if [[ ${#extra[@]} -gt 0 && "$mode" == report ]]; then echo "$pkg: manifest has ${#extra[@]} deps not in hint:" printf '%s\n' "${extra[@]}" fi if [[ ${#changed_idx[@]} -eq 0 ]]; then [[ "$mode" == report ]] && echo " $pkg: bundled deps all current" return 0 fi if [[ "$mode" == report ]]; then echo "" echo "$pkg bundled deps changed upstream:" for (( i=0; i<${#changed_idx[@]}; i++ )); do printf ' %s -> %s\n' "${changed_from[$i]##*/}" "${changed_to[$i]##*/}" done return 0 fi # apply mode: recompute md5 only for changed lines, rewrite DOWNLOAD+MD5SUM. # Back up first (mkhint convention) — only reached when there is something # to write, since the no-change case already returned above. cp "$hint" "${hint}.bak" local -a new_md5s=("${md5s[@]}") local idx md5 for idx in "${changed_idx[@]}"; do echo "Downloading (bundled): ${new_urls[$idx]}" if md5=$(download_file "${new_urls[$idx]}"); then new_md5s[$idx]="$md5" else echo " download failed for ${new_urls[$idx]} — left as-is" new_urls[$idx]="${urls[$idx]}" # revert this line fi done local new_dl new_md5v new_dl=$(build_multiline_value new_urls); new_dl="${new_dl#\"}"; new_dl="${new_dl%\"}" new_md5v=$(build_multiline_value new_md5s); new_md5v="${new_md5v#\"}"; new_md5v="${new_md5v%\"}" perl -i -0pe 'BEGIN{$v=shift} s|^DOWNLOAD="[^"]*(?:\\\n[^"]*)*"|DOWNLOAD="$v"|m' "$new_dl" "$hint" perl -i -0pe 'BEGIN{$v=shift} s|^MD5SUM="[^"]*(?:\\\n[^"]*)*"|MD5SUM="$v"|m' "$new_md5v" "$hint" } # ── end bundled-dep manifest handling ───────────────────────────────────────── # 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 # auto-strip -current phantom deps present in the .info REQUIRES load_phantom_deps local phantom; phantom=$(phantom_deps_in_info "$info") if [[ -n "$phantom" ]]; then printf 'DELREQUIRES="%s"\n' "$phantom" >> "$normalized_file" echo "added DELREQUIRES=\"$phantom\" (unneeded on -current)." fi if [[ -n "$VERSION" ]]; then local old_version old_version=$(grep '^VERSION=' "$normalized_file" | sed 's/VERSION="//;s/"$//') # bump both '_' and '-' variants (see update_hint_file note) sed -i "s/${old_version}/${VERSION}/g" "$normalized_file" if [[ "$old_version" == *_* || "$VERSION" == *_* ]]; then sed -i "s/${old_version//_/-}/${VERSION//_/-}/g" "$normalized_file" fi 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." load_bundle_manifests local _bm_pkg="${normalized_file%.hint}" _bm_pkg="${_bm_pkg##*/}" if pkg_has_manifest "$_bm_pkg"; then local _bm_ver; _bm_ver=$(grep '^VERSION=' "$normalized_file" | sed 's/VERSION="//;s/"$//') local _bm_url; _bm_url=$(manifest_url_for "$_bm_pkg" "$_bm_ver") echo "" echo "$_bm_pkg: bundled-dep manifest check" reconcile_bundle_deps "$_bm_pkg" "$normalized_file" "$_bm_url" report || true fi 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 ${label} 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 } latest=$(_normalize_version "$latest") # 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. # SlackBuild VERSION uses '_' but download URLs often carry the upstream # '-' form (dates like 2026-07-07, tags), so bump both variants — otherwise # a dated URL keeps pointing at the old tarball and its md5 goes stale. sed -i "s/${old_version}/${new_version}/g" "$normalized_file" if [[ "$old_version" == *_* || "$new_version" == *_* ]]; then sed -i "s/${old_version//_/-}/${new_version//_/-}/g" "$normalized_file" fi 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 "==========================================" } # True if a built package (.txz) for exists in the repo. pkg_in_repo() { local pkg="$1" compgen -G "${PACKAGES_DIR%/}/*/${pkg}/${pkg}-*.txz" >/dev/null 2>&1 } # Prompt to run `slackrepo `; no-op on empty list. run_slackrepo() { local action="$1"; shift [[ $# -eq 0 ]] && return 0 local answer read -r -p "Run 'slackrepo $action $*'? [Y/n] " answer answer="${answer:-Y}" if [[ "$answer" =~ ^[Yy]$ ]]; then slackrepo "$action" "$@" fi } # Single-package dispatch: update if already built, else build. prompt_slackrepo() { local pkg="$1" if pkg_in_repo "$pkg"; then run_slackrepo update "$pkg" else run_slackrepo build "$pkg" fi } # Remove a hint file and its .bak if present. No existence guard, no exit — # safe to call inside loops. Echoes what was removed. _remove_hint() { local full_path="$1" 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 } # 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 _remove_hint "$full_path" } # 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. With exactly one explicit target, -e limits # the run to that entry; otherwise scan the whole config in one pass. echo "Running nvchecker..." if [[ ${#targets[@]} -eq 1 && $# -gt 0 ]]; then nvchecker -c "$NVCHECKER_CONFIG" -e "${targets[0]}" >&2 || true else nvchecker -c "$NVCHECKER_CONFIG" >&2 || true fi # Classify each target local outdated_pkgs=() outdated_old=() outdated_new=() outdated_flag=() local missing_sections=() 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 if ! latest=$(nvchecker_latest "$pkg"); then if _has_nvchecker_section "$pkg"; then echo "skip ${pkg}: no nvchecker result" else echo "skip ${pkg}: no nvchecker section" missing_sections+=("$pkg") fi continue fi local latest_norm; latest_norm=$(_normalize_version "$latest") [[ "$current" == "$latest_norm" ]] && continue # up to date # determine direction with sort -V (compare normalized forms) local newest; newest=$(printf '%s\n%s\n' "$current" "$latest_norm" | sort -V | tail -1) local flag="update" [[ "$newest" == "$current" ]] && flag="?downgrade" outdated_pkgs+=("$pkg") outdated_old+=("$current") outdated_new+=("$latest_norm") outdated_flag+=("$flag") done # Offer to populate nvchecker.toml for packages with no section if [[ ${#missing_sections[@]} -gt 0 ]]; then echo "" echo "${#missing_sections[@]} package(s) have no nvchecker section: ${missing_sections[*]}" local answer read -r -p "Populate ${NVCHECKER_CONFIG} now? [Y/n] " answer answer="${answer:-Y}" if [[ "$answer" =~ ^[Yy]$ ]]; then local mp info for mp in "${missing_sections[@]}"; do info=$(find "$REPO_DIR" -mindepth 2 -name "${mp}.info" 2>/dev/null | head -1) if [[ -z "$info" ]]; then echo "skip ${mp}: no .info found in $REPO_DIR" continue fi add_nvchecker_section "$mp" "$info" done echo "" echo "Sections added. Review $NVCHECKER_CONFIG (fill any stubs), then re-run 'mkhint -C'." return 0 fi fi local updated=() if [[ ${#outdated_pkgs[@]} -eq 0 ]]; then echo "all up to date" else # 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 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 # Split updated packages: existing → update, new → build. if [[ ${#updated[@]} -gt 0 ]]; then local -a existing=() fresh=() local up for up in "${updated[@]}"; do if pkg_in_repo "$up"; then existing+=("$up"); else fresh+=("$up"); fi done run_slackrepo update "${existing[@]}" run_slackrepo build "${fresh[@]}" fi fi # Phase 2: bundled-dep manifest reconcile for listed packages. load_bundle_manifests local was_updated for pkg in "${targets[@]}"; do pkg_has_manifest "$pkg" || continue local hintpath="${HINT_DIR%/}/${pkg}.hint" [[ -f "$hintpath" ]] || continue was_updated=0 local u for u in "${updated[@]}"; do [[ "$u" == "$pkg" ]] && was_updated=1; done if [[ $was_updated -eq 0 && $FORCE -ne 1 ]]; then continue fi local cur; cur=$(grep '^VERSION=' "$hintpath" | sed 's/VERSION="//;s/"$//') local murl; murl=$(manifest_url_for "$pkg" "$cur") || continue echo "" echo "Bundled-dep reconcile: $pkg (manifest @ $cur)" reconcile_bundle_deps "$pkg" "$hintpath" "$murl" report || true local ans read -r -p "Apply bundled-dep updates for $pkg? [Y/n] " ans ans="${ans:-Y}" if [[ "$ans" =~ ^[Yy]$ ]]; then reconcile_bundle_deps "$pkg" "$hintpath" "$murl" apply || true fi done } # Main function main() { local parsed parsed=$(getopt -o vV:f:n:lcCdNhRF \ --long version,set-version:,hintfile:,new:,list,clean,check,delete,no-dl,help,review,fix-current,force \ -n 'mkhint' -- "$@") || { show_help; exit 1; } eval set -- "$parsed" while true; do case "$1" in --version|-v) echo "mkhint $MKHINT_VERSION" exit 0 ;; --set-version|-V) VERSION="$2" shift 2 ;; --hintfile|-f) HINT_FILE="$2" shift 2 ;; --new|-n) NEW_HINT_FILE="$2" shift 2 ;; --list|-l) SHOW_LIST=1 shift ;; --review|-R) RUN_REVIEW=1 shift ;; --clean|-c) COMMAND="clean" shift ;; --check|-C) COMMAND="check" shift ;; --fix-current|-F) COMMAND="fix-current" shift ;; --delete|-d) COMMAND="delete" shift ;; --no-dl|-N) NO_DL=1 shift ;; --force) FORCE=1 shift ;; --help|-h) COMMAND="help" shift ;; --) shift break ;; *) echo "Unknown option: $1" >&2 show_help exit 1 ;; esac done # Collect remaining positional args. # When -R is active and no other command set, they are review targets; # otherwise they are delete targets. while [[ $# -gt 0 ]]; do if [[ -n "$RUN_REVIEW" && -z "$COMMAND" ]]; then REVIEW_PKGS+=("$1") elif [[ -n "$SHOW_LIST" && -z "$COMMAND" ]]; then LIST_PKGS+=("$1") else DELETE_HINT_FILES+=("$1") fi 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 --set-version/--hintfile/--new" >&2 exit 1 fi if [[ "$COMMAND" == "fix-current" && ( -n "$VERSION" || -n "$HINT_FILE" || -n "$NEW_HINT_FILE" ) ]]; then echo "Error: --fix-current cannot be combined with --set-version/--hintfile/--new" >&2 exit 1 fi if [[ $FORCE -eq 1 && "$COMMAND" != "check" ]]; then echo "Error: --force is only valid with --check" >&2 exit 1 fi if [[ -n "$SHOW_LIST" && ${#LIST_PKGS[@]} -gt 0 ]]; then local p for p in "${LIST_PKGS[@]}"; do if [[ ! -f "${HINT_DIR%/}/${p}.hint" ]]; then echo "Error: hint file not found: ${HINT_DIR%/}/${p}.hint" >&2 exit 2 fi done for p in "${LIST_PKGS[@]}"; do _show_hint_diff "$p" done exit 0 fi if [[ -n "$SHOW_LIST" || -n "$RUN_REVIEW" ]]; then [[ -n "$SHOW_LIST" ]] && list_hint_files if [[ -n "$RUN_REVIEW" ]]; then if [[ ${#REVIEW_PKGS[@]} -gt 0 ]]; then review_hint_files "${REVIEW_PKGS[@]}" else # ensure MATCHED_PKGS is populated even when -l was not given [[ -z "$SHOW_LIST" ]] && list_hint_files >/dev/null review_hint_files fi fi exit $? fi case "$COMMAND" in help) show_help ;; clean) clean_bak_files ;; check) check_updates "${DELETE_HINT_FILES[@]}" ;; fix-current) fix_current ;; 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 load_bundle_manifests local _bm_pkg="${HINT_FILE%.hint}" _bm_pkg="${_bm_pkg##*/}" if pkg_has_manifest "$_bm_pkg"; then local _bm_hint="${HINT_DIR%/}/${_bm_pkg}.hint" local _bm_cur; _bm_cur=$(grep '^VERSION=' "$_bm_hint" | sed 's/VERSION="//;s/"$//') local _bm_url; _bm_url=$(manifest_url_for "$_bm_pkg" "$_bm_cur") echo "" echo "Bundled-dep reconcile: $_bm_pkg (manifest @ $_bm_cur)" reconcile_bundle_deps "$_bm_pkg" "$_bm_hint" "$_bm_url" report || true local _bm_ans read -r -p "Apply bundled-dep updates for $_bm_pkg? [Y/n] " _bm_ans _bm_ans="${_bm_ans:-Y}" if [[ "$_bm_ans" =~ ^[Yy]$ ]]; then reconcile_bundle_deps "$_bm_pkg" "$_bm_hint" "$_bm_url" apply || true fi 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 } # Only run main when executed directly; allow tests to source this file to # reach individual functions without triggering the CLI (and its `exit 1` # for a no-args invocation). if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then main "$@" fi