master chld/dot / bin / dtr~
   1#!/bin/sh -u
   2# dtr - ports manager
   3
   4readonly PROG_NAME="${0##*/}"
   5readonly PROG_VERSION="1.2.1"
   6
   7: "${DTR_DIR:=/ports}"
   8: "${DTR_VERBOSITY:=1}"
   9: "${DTR_MAKE:=dmake.sh}"
  10: "${DTR_NEW_MAKE:=ndmake.sh}"
  11: "${DTR_SKIP_CONFIRM:=0}"
  12: "${DTR_REMOTE:=origin}"
  13: "${DTR_BRANCH:=main}"
  14: "${DTR_CACHE:=/var/cache/dtr}"
  15: "${SPC_EXT:=tzst}"
  16: "${BUILDBASE:=/var/tmp/dtr}"
  17: "${TMPDIR:=/tmp}"
  18: "${NO_COLOR:=}"
  19
  20readonly DEFAULT_REPO="https://codeberg.org/derivelinux/ports"
  21readonly EXTENSION_DIRS="/share/dtr /usr/share/dtr"
  22readonly OVERLAY_FILE="overlay"
  23
  24# === FORMAT STRINGS ===
  25# highlight:
  26readonly _str_highlight_o="<ul><bold>"
  27readonly _str_highlight_c="</ul></bold>"
  28readonly _str_highlight_f="${_str_highlight_o}%s${_str_highlight_c}"
  29
  30# === LOGGING ===
  31strip_tags() { printf "$@" | sed 's/<[^>]*>//g'; }
  32
  33log_printf="strip_tags"
  34if [ -z "$NO_COLOR" ] && _cmd="$(command -v tmlf 2>/dev/null)"; then
  35    log_printf="$_cmd"
  36fi
  37
  38msg() {
  39    [ "$DTR_VERBOSITY" -lt 1 ] && return
  40    $log_printf '<green>==></green> %s\n' "$*" >&2
  41}
  42info() {
  43    [ "$DTR_VERBOSITY" -lt 1 ] && return
  44    $log_printf '<dim>)</dim>    %s\n' "$*" >&2
  45}
  46die() { $log_printf '%s: <bg-red>error</bg-red>: %s\n' "$PROG_NAME" "$*" >&2; exit 1; }
  47warn() { $log_printf '%s: <bg-yellow>warning</bg-yellow>: %s\n' "$PROG_NAME" "$*" >&2; }
  48
  49# === UTILITIES ===
  50chill() { head -n 32 "$1" | tail -n 31 | sed 's/^#//'; }
  51confirm_action() {
  52    [ "$DTR_SKIP_CONFIRM" -eq 1 ] && return 0
  53    [ -t 0 ] && [ -t 1 ] || return 1
  54    $log_printf '%s [y/N] ' "$1"
  55    read -r ans </dev/tty
  56    case "$ans" in [Yy]*) return 0 ;; *) return 1 ;; esac
  57}
  58for_each_line() {
  59    while IFS= read -r line; do
  60        [ -n "$line" ] || continue
  61        "$@" "$line"
  62    done
  63}
  64tmp_file() { printf '%s/dtr-%s-%s' "$TMPDIR" "$1" "$$"; }
  65strip_md_link() { printf '%s' "$1" | sed 's/\[\([^]]*\)\]([^)]*)/\1/g'; }
  66
  67# === STRACE PARSING (LEGACY SUPPORT) ===
  68parse_strace() {
  69    awk '
  70        / = -1 / {next}
  71        function extract_path(line) {
  72            start = index(line, "<")
  73            if (start == 0) return ""
  74            rest = substr(line, start + 1)
  75            end = index(rest, ">")
  76            if (end == 0) return ""
  77            return substr(rest, 1, end - 1)
  78        }
  79        /(open|openat|creat)\(.*(O_CREAT|O_WRONLY)/ ||
  80         /(rename|renameat|renameat2|symlink|symlinkat|link|linkat)/ {
  81            if ($0 ~ /= [0-9]+</) {
  82                path = extract_path($0)
  83                # Only track typical system paths to avoid noise
  84                if (path ~ /^\/(usr|etc|lib|bin|var|opt|sbin)/) print path
  85            }
  86        }
  87    ' "$1" | sort -u
  88}
  89
  90# === PACKAGE DISCOVERY ===
  91find_package() {
  92    local res
  93    case "$1" in
  94        /*)
  95            if [ -d "$1" ] && [ -f "$1/$DTR_NEW_MAKE" ]; then
  96                printf '%s' "$1"
  97                return 0
  98            fi
  99            return 1
 100            ;;
 101        *)
 102            res=$(find "$DTR_DIR" -mindepth 2 -maxdepth 2 -type d -name "$1" -print -quit 2>/dev/null)
 103            if [ -n "$res" ] && [ -f "$res/$DTR_NEW_MAKE" ]; then
 104                printf '%s' "$res"
 105                return 0
 106            fi
 107            return 1
 108            ;;
 109    esac
 110}
 111
 112get_makefile() {
 113    local dir="$1"
 114    if [ -f "$dir/$DTR_NEW_MAKE" ]; then
 115        echo "$dir/$DTR_NEW_MAKE"
 116    else
 117        return 1
 118    fi
 119}
 120
 121resolve_package_arg() {
 122    case "$1" in
 123        /*) printf '%s' "$1"; return ;;
 124    esac
 125
 126    local found
 127    found=$(find_package "$1")
 128    if [ -n "$found" ]; then
 129        printf '%s' "$found"
 130        return
 131    fi
 132
 133    if [ -d "$1" ]; then
 134        cd "$1" && pwd
 135        return
 136    fi
 137
 138    printf '%s' "$1"
 139}
 140
 141# Check installed status via spc
 142is_installed() {
 143    spc info "${1##*/}" >/dev/null 2>&1
 144}
 145
 146# === METADATA ===
 147get_package_version() {
 148    local dir makefile
 149    dir=$(find_package "$1") || return 1
 150    makefile=$(get_makefile "$dir") || return 1
 151    grep '^VERSION=' "$makefile" 2>/dev/null |
 152        head -n1 | cut -d= -f2- | tr -d '"'
 153}
 154get_package_release() {
 155    local dir makefile
 156    dir=$(find_package "$1") || return 1
 157    makefile=$(get_makefile "$dir") || return 1
 158    local rel
 159    rel=$(grep '^RELEASE=' "$makefile" 2>/dev/null |
 160        head -n1 | cut -d= -f2- | tr -d '"')
 161    echo "${rel:-1}"
 162}
 163# Extract NAME explicitly, falling back to empty if not found
 164get_package_name_var() {
 165    local dir makefile
 166    dir=$(find_package "$1") || return 1
 167    makefile=$(get_makefile "$dir") || return 1
 168    grep '^NAME=' "$makefile" 2>/dev/null |
 169        head -n1 | cut -d= -f2- | tr -d '"'
 170}
 171get_package_commit() {
 172    local dir repo makefile
 173    dir=$(find_package "$1") || return 1
 174    makefile=$(get_makefile "$dir") || return 1
 175    repo=$(grep '^REPO=' "$makefile" 2>/dev/null |
 176           head -n1 | cut -d= -f2- | tr -d '"')
 177    [ -n "$repo" ] || return 1
 178    git ls-remote --quiet "$repo" HEAD 2>/dev/null | cut -f1
 179}
 180get_package_source() {
 181    local dir makefile
 182    dir=$(find_package "$1") || return 1
 183    makefile=$(get_makefile "$dir") || return 1
 184    awk '
 185        /^SOURCE=/{
 186            line=substr($0, index($0,"=")+1)
 187            while (line ~ /\\$/) {
 188                sub(/\\$/, "", line)
 189                if (getline > 0) {
 190                    sub(/^[ \t]*/, "", $0)
 191                    line=line $0
 192                } else {
 193                    break
 194                }
 195            }
 196            sub(/^"/, "", line)
 197            sub(/"$/, "", line)
 198            print line
 199            exit
 200        }
 201    ' "$makefile"
 202}
 203get_package_git_url() {
 204    local src tok
 205    src=$(get_package_source "$1") || return 1
 206    for tok in $src; do
 207        tok="${tok%%::*}"
 208        case "$tok" in
 209            *git@*:*|git://*|*://*.git|*://*/.git)
 210                printf '%s\n' "$tok"
 211                return 0
 212                ;;
 213        esac
 214    done
 215    return 1
 216}
 217resolve_git_version() {
 218    local ver repo commit
 219    ver="$1"
 220    case "$ver" in
 221        git:*) printf '%s\n' "$ver"; return 0 ;;
 222        git)
 223            repo=$(get_package_git_url "$2" || true)
 224            if [ -n "$repo" ]; then
 225                commit=$(git ls-remote --quiet "$repo" HEAD 2>/dev/null | cut -f1)
 226                [ -n "$commit" ] && { printf 'git:%s\n' "$commit"; return 0; }
 227            fi
 228            ;;
 229    esac
 230    printf '%s\n' "$ver"
 231}
 232get_info_field() {
 233    local infofile="$1" field="$2"
 234    [ -f "$infofile" ] || return 1
 235    grep "^${field}:" "$infofile" | head -n1 | cut -d: -f2- | sed 's/^ *//'
 236}
 237format_package_info() {
 238    local pkg="$1" fmt="$2"
 239    local dir infofile name="" desc="" maint="" ver="" contributors=""
 240
 241    dir=$(find_package "$pkg")
 242    if [ -n "$dir" ]; then
 243        infofile="$dir/info"
 244        ver=$(get_package_version "$pkg")
 245        ver=$(resolve_git_version "$ver" "$pkg")
 246
 247        if [ -f "$infofile" ]; then
 248            name=$(get_info_field "$infofile" "name")
 249            desc=$(get_info_field "$infofile" "description")
 250            maint=$(get_info_field "$infofile" "maintainer")
 251            contributors=$(get_info_field "$infofile" "contributors")
 252
 253            [ -n "$maint" ] && maint=$(strip_md_link "$maint")
 254            [ -n "$contributors" ] && contributors=$(strip_md_link "$contributors")
 255        fi
 256    fi
 257
 258    [ -n "$ver" ] && ver="+$ver"
 259    [ -n "$maint" ] && maint=":$maint"
 260    [ -n "$desc" ] && desc=" - $desc"
 261
 262    eval "echo $fmt"
 263}
 264
 265# === DEPENDENCIES ===
 266parse_dep_name() {
 267    local dep
 268    dep=$(printf '%s' "$1" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
 269    case "$dep" in
 270        [/.]*)  printf '%s' "${dep#?}" ;;
 271        \>*)    printf '%s' "${dep#?}" ;;
 272        *)      printf '%s' "$dep" ;;
 273    esac
 274}
 275parse_dep_type() {
 276    local dep
 277    dep=$(printf '%s' "$1" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
 278    case "$dep" in
 279        /*)  printf 'link' ;;
 280        \>*) printf 'runtime' ;;
 281        .*)  printf 'build' ;;
 282        *)   printf 'link' ;;
 283    esac
 284}
 285read_deps_file() {
 286    local depsfile="$1" dep
 287    [ -f "$depsfile" ] || return
 288
 289    while IFS= read -r dep; do
 290        dep=$(printf '%s' "$dep" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
 291        case "$dep" in ''|\#*) continue ;; esac
 292        printf '%s\n' "$dep"
 293    done < "$depsfile"
 294}
 295
 296# VISUALIZATION
 297# print_dep_tree <pkg> [prefix] [is_last] [visited_file] [mode] [extra]
 298print_dep_tree() {
 299    local pkg="$1"
 300    local prefix="${2:-}"
 301    local is_last="${3:-1}" # 1 = yes, 0 = no
 302    local visited_file="${4:-}"
 303    local mode="${5:-fwd}"
 304    local extra="${6:-}"
 305    
 306    local is_toplevel=0
 307    if [ -z "$visited_file" ]; then
 308    # Setup visited tracking
 309        visited_file=$(tmp_file "tree-visited")
 310        > "$visited_file"
 311        is_toplevel=1
 312        case "$mode" in
 313            upg) : ;;
 314            rem) $log_printf "<bold>%s</bold> [remove]\n" "${pkg##*/}" ;;
 315            *)   $log_printf "<bold>%s</bold>\n" "$pkg" ;;
 316        esac
 317        [ "$mode" != "upg" ] && echo "${pkg##*/}" >> "$visited_file"
 318    fi
 319
 320    local children="" nl='
 321'
 322    case "$mode" in
 323        fwd)
 324            local dir depsfile
 325            dir=$(find_package "$pkg") || return
 326            depsfile="$dir/deps"
 327            [ -f "$depsfile" ] && children=$(read_deps_file "$depsfile")
 328            ;;
 329        rev)
 330            local ilist="/tmp/dtr-installed-$$"
 331            [ -f "$ilist" ] || spc list | cut -d'+' -f1 > "$ilist"
 332            local short_pkg="${pkg##*/}"
 333            while IFS= read -r rd; do
 334                [ -n "$rd" ] || continue
 335                local rdir; rdir=$(find_package "$rd") || continue
 336                local rdf="$rdir/deps"
 337                [ -f "$rdf" ] || continue
 338                local line; line=$(grep -E "^[/>.]?${short_pkg}$" "$rdf" | head -n1)
 339                if [ -n "$line" ]; then
 340                    local type; type=$(parse_dep_type "$line")
 341                    children="${children}${rd}|${type}${nl}"
 342                fi
 343            done < "$ilist"
 344            [ "$is_toplevel" -eq 1 ] && rm -f "$ilist"
 345            ;;
 346        upg)
 347            [ -f "$extra" ] && children=$(cat "$extra" | sort)
 348            ;;
 349        rem)
 350            local short_pkg="${pkg##*/}"
 351            local lds; lds=$(spc info "$short_pkg" 2>/dev/null | awk '/^deps:/{p=1;next}/^files:/{p=0}p&&NF{print}') || :
 352            for dl in $lds; do
 353                local dn; dn=$(parse_dep_name "$dl")
 354                local dt; dt=$(parse_dep_type "$dl")
 355                local rb; rb=$(spc info "$dn" 2>/dev/null | grep "^required-by:" | cut -d: -f2-)
 356                local isex=1
 357                for r in $rb; do
 358                    if ! grep -qxF "$r" "$visited_file"; then
 359                        isex=0; break
 360                    fi
 361                done
 362                if [ "$isex" -eq 1 ]; then
 363                    children="${children}${dn}|${dt}${nl}"
 364                fi
 365            done
 366            ;;
 367    esac
 368
 369    [ -n "$children" ] || return
 370    if [ "$mode" = "rev" ] || [ "$mode" = "rem" ]; then
 371        children=$(printf '%s' "$children" | sort)
 372    fi
 373    
 374    local total; total=$(printf '%s' "$children" | grep -c . || echo 0)
 375    local count=0; local old_ifs="$IFS"; IFS="$nl"
 376    for info in $children; do
 377        IFS="$old_ifs"; [ -n "$info" ] || continue
 378        count=$((count + 1))
 379        
 380        local connector="├─"; local new_prefix="${prefix}│  "
 381        [ "$count" -eq "$total" ] && { connector="└─"; new_prefix="${prefix}   "; }
 382        
 383        local c_name="" c_type="" label_main="" status="" recurse=1 next_mode="$mode"
 384        if [ "$mode" = "upg" ]; then
 385            local p t o n
 386            p=$(printf '%s' "$info" | cut -d'|' -f1); t=$(printf '%s' "$info" | cut -d'|' -f2)
 387            o=$(printf '%s' "$info" | cut -d'|' -f3); n=$(printf '%s' "$info" | cut -d'|' -f4)
 388            c_name="$p"; c_type="$t"
 389            if [ "$t" = "version" ]; then
 390                label_main="<bold>$p</bold> [$o -> $n]"
 391            else
 392                label_main=$(printf '<bold>%s</bold> [%.8s -> %.8s]' "$p" "$o" "$n")
 393            fi
 394            echo "$c_name" >> "$visited_file"; next_mode="rev"
 395        else
 396            if [ "$mode" = "fwd" ]; then
 397                c_name=$(parse_dep_name "$info"); c_type=$(parse_dep_type "$info")
 398                if grep -qxF "$c_name" "$visited_file"; then
 399                    status=" <dim>(...)</dim>"; recurse=0
 400                else
 401                    echo "$c_name" >> "$visited_file"
 402                    if is_installed "$c_name"; then status=" <green>[installed]</green>"; else status=" <yellow>[new]</yellow>"; fi
 403                fi
 404            elif [ "$mode" = "rev" ]; then
 405                c_name=$(printf '%s' "$info" | cut -d'|' -f1); c_type=$(printf '%s' "$info" | cut -d'|' -f2)
 406                if grep -qxF "$c_name" "$visited_file"; then
 407                    status=" <dim>(...)</dim>"; recurse=0
 408                else
 409                    echo "$c_name" >> "$visited_file"; status=" <yellow>[rebuild]</yellow>"
 410                fi
 411            elif [ "$mode" = "rem" ]; then
 412                c_name=$(printf '%s' "$info" | cut -d'|' -f1); c_type=$(printf '%s' "$info" | cut -d'|' -f2)
 413                if grep -qxF "$c_name" "$visited_file"; then
 414                    status=" <dim>(...)</dim>"; recurse=0
 415                else
 416                    echo "$c_name" >> "$visited_file"; status=" <yellow>[orphan]</yellow>"
 417                fi
 418            fi
 419            label_main="<bold>$c_name</bold>"
 420        fi
 421        
 422        local t_mk=""
 423        case "$c_type" in
 424            link)    t_mk=" <dim>[link-time]</dim>" ;; 
 425            runtime) t_mk=" <dim>[runtime]</dim>" ;;
 426            build)   t_mk=" <dim>[build-time]</dim>" ;;
 427        esac
 428        
 429        $log_printf "%s%s %s%s%s\n" "$prefix" "$connector" "$label_main" "$t_mk" "$status"
 430        [ "$recurse" -eq 1 ] && print_dep_tree "$c_name" "$new_prefix" 1 "$visited_file" "$next_mode"
 431        IFS="$nl"
 432    done
 433    IFS="$old_ifs"
 434    [ "$is_toplevel" -eq 1 ] && rm -f "$visited_file"
 435}
 436
 437build_ordered_deps() {
 438    local target="$1" seen order
 439    seen=$(tmp_file "seen")
 440    order=$(tmp_file "order")
 441
 442    > "$seen"
 443    > "$order"
 444    _build_deps_recursive "$target" "$seen" "$order"
 445    cat "$order"
 446    rm -f "$seen" "$order"
 447}
 448_build_deps_recursive() {
 449    local target="$1" seen="$2" order="$3" dir depsfile dep name
 450
 451    grep -qxF "$target" "$seen" 2>/dev/null && return
 452    printf '%s\n' "$target" >> "$seen"
 453
 454    dir=$(find_package "$target") || return 0
 455    depsfile="$dir/deps"
 456
 457    if [ -f "$depsfile" ]; then
 458        while IFS= read -r dep; do
 459            case "$dep" in ''|'#'*) continue ;; esac
 460            name=$(parse_dep_name "$dep")
 461            [ -n "$name" ] || continue
 462            _build_deps_recursive "$name" "$seen" "$order"
 463        done < "$depsfile"
 464    fi
 465
 466    # Add to build order if NOT installed
 467    is_installed "$target" || printf '%s\n' "$target" >> "$order"
 468}
 469
 470find_reverse_deps() {
 471    local target="$1" filter_type="${2:-}" pkg pkgdir depsfile dep name type
 472
 473    # Use spc to list all installed packages
 474    spc list | cut -d'+' -f1 | while IFS= read -r pkg; do
 475        pkgdir=$(find_package "$pkg") || continue
 476        depsfile="$pkgdir/deps"
 477
 478        read_deps_file "$depsfile" | while IFS= read -r dep; do
 479            name=$(parse_dep_name "$dep")
 480            [ "$name" = "$target" ] || continue
 481
 482            if [ -n "$filter_type" ]; then
 483                type=$(parse_dep_type "$dep")
 484                [ "$type" = "$filter_type" ] || continue
 485            fi
 486
 487            printf '%s\n' "$pkg"
 488            break
 489        done
 490    done
 491}
 492rebuild_dependents() {
 493    local target="$1" filter_type="${2:-}" rebuilt countf dep is_toplevel=0
 494    rebuilt=$(tmp_file "rebuilt")
 495    countf=$(tmp_file "count")
 496
 497    [ -f "$rebuilt" ] || { > "$rebuilt"; echo 0 > "$countf"; is_toplevel=1; }
 498    grep -qxF "$target" "$rebuilt" && { cat "$countf" 2>/dev/null || echo 0; return; }
 499
 500    printf '%s\n' "$target" >> "$rebuilt"
 501
 502    find_reverse_deps "$target" "$filter_type" | while IFS= read -r dep; do
 503        [ -n "$dep" ] || continue
 504
 505        msg "rebuilding dependent: $dep"
 506
 507        # clean first so cached source etc doesent cause skip
 508        clean_package "$dep" || die "failed to clean dependent: $dep"
 509
 510        # rebuild deps non-interactively, and die fast on fail.
 511        DTR_SKIP_CONFIRM=1 do_make "$dep" || die "failed to rebuild dependent: $dep"
 512        install_package "$dep" || die "failed to install rebuilt dependent: $dep"
 513
 514        local count
 515        count=$(cat "$countf")
 516        count=$((count + 1))
 517        echo "$count" > "$countf"
 518
 519        rebuild_dependents "$dep" "$filter_type" >/dev/null
 520    done
 521
 522    cat "$countf" 2>/dev/null || echo 0
 523    [ "$is_toplevel" -eq 1 ] && rm -f "$rebuilt" "$countf"
 524}
 525
 526# === BUILD & INSTALL ===
 527build_package() {
 528    local pkg="$1" dir makefile
 529
 530    [ -z "$pkg" ] && die "build_package called with empty package name"
 531
 532    dir=$(find_package "$pkg") || die "package $pkg not found"
 533    makefile=$(get_makefile "$dir") || die "no build script found for $pkg"
 534
 535    # CACHE INVALIDATION
 536    # If we are building, we must invalidate the cache to prevent 'install'
 537    # from picking up the old package instead of the new build artifacts.
 538    local pkgname ver rel pkg_archive
 539    pkgname=$(get_package_name_var "$pkg")
 540    [ -z "$pkgname" ] && pkgname="${dir##*/}"
 541    ver=$(get_package_version "$pkg")
 542    ver=$(resolve_git_version "$ver" "$pkg")
 543    rel=$(get_package_release "$pkg")
 544
 545    if [ -n "$ver" ]; then
 546        pkg_archive="${DTR_CACHE}/${pkgname}+${ver}-${rel}.spc.${SPC_EXT}"
 547        if [ -f "$pkg_archive" ]; then
 548            rm -f "$pkg_archive"
 549            info "invalidated cache for $pkgname"
 550        fi
 551    fi
 552
 553    msg "building $pkg"
 554    cd "$dir" || die "cannot cd to $dir"
 555    sh "$makefile" make || die "make failed for $pkg"
 556}
 557
 558check_cache_validity() {
 559    local pkg="$1" cache_file="$2"
 560    [ -f "$cache_file" ] || return 1
 561    
 562    local dir depsfile
 563    dir=$(find_package "$pkg") || return 0
 564    depsfile="$dir/deps"
 565    [ -f "$depsfile" ] || return 0
 566
 567    local cache_mtime
 568    cache_mtime=$(stat -c %Y "$cache_file")
 569
 570    # Check link-time dependencies
 571    while IFS= read -r line; do
 572        [ -z "$line" ] && continue
 573        case "$line" in \#*) continue ;; esac
 574        
 575        local type name
 576        type=$(parse_dep_type "$line")
 577        name=$(parse_dep_name "$line")
 578
 579        # Only care about link-time deps (default or /)
 580        if [ "$type" = "link" ]; then
 581            # Check installed lockfile timestamp
 582            local lockfile="/var/lib/spc/lock/${name}.lock"
 583            if [ -f "$lockfile" ]; then
 584                local lock_mtime
 585                lock_mtime=$(stat -c %Y "$lockfile")
 586                if [ "$lock_mtime" -gt "$cache_mtime" ]; then
 587                    info "cache invalid: dependency $name is newer than cached $pkg"
 588                    return 1
 589                fi
 590            fi
 591        fi
 592    done < "$depsfile"
 593    return 0
 594}
 595
 596install_package() {
 597    local pkg="$1" dir makefile pkgname ver_raw ver rel staging_dir pkg_archive is_legacy=0
 598    # Optional: space-separated list of packages that requested this install
 599    local required_by="${2:-}"
 600
 601    dir=$(find_package "$pkg") || die "package $pkg not found"
 602    makefile=$(get_makefile "$dir") || die "no build script found for $pkg"
 603
 604    if [ "${makefile##*/}" = "$DTR_MAKE" ]; then
 605        is_legacy=1
 606    fi
 607
 608    pkgname=$(get_package_name_var "$pkg")
 609    [ -z "$pkgname" ] && pkgname="${dir##*/}"
 610
 611    ver_raw=$(get_package_version "$pkg")
 612    ver=$(resolve_git_version "$ver_raw" "$pkg")
 613    rel=$(get_package_release "$pkg")
 614    [ -z "$ver_raw" ] && die "cannot determine version for $pkg"
 615
 616    pkg_archive="${DTR_CACHE}/${pkgname}+${ver}-${rel}.spc.${SPC_EXT}"
 617
 618    # CACHE CHECK
 619    if [ -f "$pkg_archive" ]; then
 620        if check_cache_validity "$pkgname" "$pkg_archive"; then
 621            msg "using cached package: $pkg_archive"
 622            msg "installing $pkgname via spc..."
 623            SPC_REQUIRED_BY="$required_by" spc install "$pkg_archive" || die "spc failed to install cached package"
 624            return 0
 625        else
 626            info "cache exists but is stale (dependency update detected)"
 627            rm -f "$pkg_archive"
 628        fi
 629    fi
 630
 631    if [ "$is_legacy" -eq 1 ]; then
 632        msg "installing $pkgname (legacy mode)..."
 633        local slog nfiles
 634        slog=$(tmp_file "strace-$pkgname")
 635        nfiles=$(tmp_file "nfiles-$pkgname")
 636        staging_dir="${BUILDBASE}/pkg-legacy-${pkgname}-${ver_raw}"
 637
 638        rm -rf "$staging_dir"
 639        mkdir -p "$staging_dir"
 640
 641        cd "$dir" || die "cannot cd to $dir"
 642
 643        if ! strace -f -y -qq \
 644            -e trace=open,openat,creat,rename,renameat,renameat2,symlink,symlinkat,link,linkat \
 645            -o "$slog" sh "$makefile" install; then
 646            rm -f "$slog" "$nfiles"
 647            die "legacy install failed for $pkg"
 648        fi
 649
 650        parse_strace "$slog" > "$nfiles"
 651
 652        msg "capturing legacy files..."
 653        while read -r f; do
 654             if [ -e "$f" ]; then
 655                 mkdir -p "$staging_dir/$(dirname "$f")"
 656                 cp -a "$f" "$staging_dir/$f"
 657             fi
 658        done < "$nfiles"
 659
 660        rm -f "$slog" "$nfiles"
 661
 662        msg "packaging legacy capture..."
 663        spc create "$staging_dir" "$pkg_archive" || die "spc failed to create package"
 664
 665        msg "registering $pkgname via spc..."
 666        SPC_REQUIRED_BY="$required_by" SPC_FORCE=1 spc install "$pkg_archive" || die "spc failed to register package"
 667
 668        rm -rf "$staging_dir"
 669
 670    else
 671        staging_dir="${BUILDBASE}/pkg-${pkgname}-${ver_raw}"
 672
 673        if [ ! -d "$staging_dir" ]; then
 674            die "staging directory not found: $staging_dir (build not run?)"
 675        fi
 676
 677        msg "packaging $pkgname..."
 678        spc create "$staging_dir" "$pkg_archive" || die "spc failed to create package"
 679
 680        msg "installing $pkgname via spc..."
 681        SPC_REQUIRED_BY="$required_by" spc install "$pkg_archive" || die "spc failed to install package"
 682    fi
 683}
 684
 685clean_package() {
 686    local pkg="$1" dir makefile
 687
 688    dir=$(find_package "$pkg") || die "package $pkg not found"
 689
 690    if makefile=$(get_makefile "$dir"); then
 691        msg "cleaning $pkg"
 692        cd "$dir" || die "cannot cd to $dir"
 693        sh "$makefile" clean || die "clean failed for $pkg"
 694    else
 695        die "no build script found for $pkg"
 696    fi
 697}
 698
 699remove_package() {
 700    local pkg="$1" n="${pkg##*/}"
 701    is_installed "$n" || die "package '$n' not installed"
 702    $log_printf "\n<bold>removal impact for %s:</bold>\n" "$n"
 703    print_dep_tree "$pkg" " " 1 "" "rem"
 704    $log_printf "\n"
 705    confirm_action "remove package $n?" || return 1
 706
 707    local dir pkgname makefile
 708    dir=$(find_package "$pkg") || die "package $pkg not found"
 709    pkgname=$(get_package_name_var "$pkg")
 710    [ -z "$pkgname" ] && pkgname="$n"
 711
 712    msg "removing $pkgname"
 713    spc remove "$pkgname" || die "spc failed to remove package"
 714
 715    if makefile=$(get_makefile "$dir"); then
 716        cd "$dir"
 717        sh "$makefile" clean >/dev/null 2>&1 || true
 718    fi
 719}
 720
 721purge_package() {
 722    local pkg="$1" n="${pkg##*/}" to_rem
 723    is_installed "$n" || die "package '$n' not installed"
 724    $log_printf "\n<bold>purge plan for %s (includes orphans):</bold>\n" "$n"
 725    to_rem=$(tmp_file "to-remove")
 726    > "$to_rem"
 727    _find_orphans_recursive "$pkg" "$to_rem"
 728    print_dep_tree "$pkg" " " 1 "" "rem"
 729    $log_printf "\n"
 730    confirm_action "remove package $n and dependencies?" || { rm -f "$to_rem"; return 1; }
 731
 732    tac "$to_rem" | while read -r p; do
 733        local pdir; pdir=$(find_package "$p") || continue
 734        msg "purging $p"
 735        spc purge "$p" || die "spc purge failed for $p"
 736        if makefile=$(get_makefile "$pdir"); then
 737            cd "$pdir"
 738            sh "$makefile" clean >/dev/null 2>&1 || true
 739        fi
 740    done
 741    rm -f "$to_rem"
 742}
 743
 744_find_orphans_recursive() {
 745    local pkg="$1" n="${pkg##*/}" out_file="$2"
 746    grep -qxF "$n" "$out_file" && return
 747    echo "$n" >> "$out_file"
 748    local lds; lds=$(spc info "$n" 2>/dev/null | awk '/^deps:/{p=1;next}/^files:/{p=0}p&&NF{print}') || :
 749    for dl in $lds; do
 750        local dn; dn=$(parse_dep_name "$dl")
 751        local rb; rb=$(spc info "$dn" 2>/dev/null | grep "^required-by:" | cut -d: -f2-)
 752        local isex=1
 753        for r in $rb; do
 754            if ! grep -qxF "$r" "$out_file"; then
 755                isex=0; break
 756            fi
 757        done
 758        [ "$isex" -eq 1 ] && _find_orphans_recursive "$dn" "$out_file"
 759    done
 760}
 761
 762do_make() {
 763    local pkg="$1" plan deps dep
 764
 765    # Check existence before proceeding
 766    if ! find_package "$pkg" >/dev/null; then
 767        die "package '$pkg' not found"
 768    fi
 769
 770    plan=$(tmp_file "plan")
 771    deps=$(build_ordered_deps "$pkg")
 772
 773    if [ -n "$deps" ]; then
 774        printf '%s\n' "$deps" > "$plan"
 775    else
 776        > "$plan"
 777    fi
 778
 779    if [ "$DTR_SKIP_CONFIRM" != "1" ]; then
 780        local is_reinstall=0
 781        local pname="${pkg##*/}"
 782        if is_installed "$pkg"; then
 783            is_reinstall=1
 784        fi
 785
 786        $log_printf '\n<bold>build plan for %s:</bold>\n' "$pname"
 787        print_dep_tree "$pkg" " " 1
 788        $log_printf "\n"
 789
 790        if [ "$is_reinstall" -eq 1 ]; then
 791             $log_printf "package ${_str_highlight_o}${pname}${_str_highlight_c} is already installed.\n"
 792             confirm_action "rebuild ${_str_highlight_o}${pname}${_str_highlight_c}?" || { rm -f "$plan"; return 1; }
 793        else
 794             confirm_action "proceed with build?" || { rm -f "$plan"; return 1; }
 795        fi
 796    fi
 797
 798    [ -f "$plan" ] && while IFS= read -r dep; do
 799        [ -n "$dep" ] && [ "$dep" != "$pkg" ] || continue
 800        is_installed "$dep" && continue
 801        msg "installing dependency: $dep"
 802        build_package "$dep"
 803        # Check how $pkg lists this dep. Build-time deps (.) are not needed
 804        # post-install so they don't generate a required-by relationship.
 805        local _req_by="${pkg##*/}"
 806        local _pkgdir
 807        _pkgdir=$(find_package "$pkg") || true
 808        if [ -n "$_pkgdir" ] && [ -f "$_pkgdir/deps" ]; then
 809            local _raw
 810            _raw=$(grep -xF ".$dep" "$_pkgdir/deps" 2>/dev/null || true)
 811            [ -n "$_raw" ] && _req_by=""
 812        fi
 813        install_package "$dep" "$_req_by"
 814    done < "$plan"
 815
 816    build_package "$pkg"
 817    rm -f "$plan"
 818}
 819
 820do_make_nodeps() {
 821    local pkg="$1"
 822
 823    # Check existence before proceeding
 824    if ! find_package "$pkg" >/dev/null; then
 825        die "package '$pkg' not found"
 826    fi
 827
 828    if [ "$DTR_SKIP_CONFIRM" != "1" ]; then
 829        local is_reinstall=0
 830        local pname="${pkg##*/}"
 831        if is_installed "$pkg"; then
 832            is_reinstall=1
 833        fi
 834
 835        $log_printf '\n<bold>building (no deps) %s:</bold>\n' "$pname"
 836
 837        if [ "$is_reinstall" -eq 1 ]; then
 838             $log_printf "package ${_str_highlight_o}${pname}${_str_highlight_c} is already installed.\n"
 839             confirm_action "rebuild ${_str_highlight_o}${pname}${_str_highlight_c}?" || return 1
 840        else
 841             confirm_action "proceed with build?" || return 1
 842        fi
 843    fi
 844
 845    build_package "$pkg"
 846}
 847
 848# === SYNC & UPGRADE ===
 849get_remote_branch() {
 850    local remote="${DTR_REMOTE}" branch="${DTR_BRANCH}" fallback="main"
 851
 852    if git show-ref --verify --quiet "refs/remotes/$remote/$branch"; then
 853        printf '%s/%s' "$remote" "$branch"
 854    elif git show-ref --verify --quiet "refs/remotes/$remote/$fallback"; then
 855        printf '%s/%s' "$remote" "$fallback"
 856    else
 857        die "neither $remote/$branch nor $remote/$fallback found"
 858    fi
 859}
 860
 861overlay_file() {
 862    printf '%s/%s' "$DTR_DIR" "$OVERLAY_FILE"
 863}
 864
 865overlay_cache() {
 866    printf '%s/overlays' "$DTR_CACHE"
 867}
 868
 869overlay_key() {
 870    printf '%s' "$1" | cksum | awk '{print $1}'
 871}
 872
 873overlay_dir() {
 874    local overlay_url="$1" key
 875    key=$(overlay_key "$overlay_url")
 876    printf '%s/%s' "$(overlay_cache)" "$key"
 877}
 878
 879valid_overlay_url() {
 880    case "$1" in
 881        http://*|https://*|git://*|ssh://*|git@*:*|*.git)
 882            return 0
 883            ;;
 884        *)
 885            return 1
 886            ;;
 887    esac
 888}
 889
 890sync_overlay_repo() {
 891    local overlay_url="$1" dir
 892    dir=$(overlay_dir "$overlay_url")
 893
 894    mkdir -p "$(overlay_cache)" || die "failed to create overlay cache directory"
 895    if [ -d "$dir/.git" ]; then
 896        msg "syncing overlay repo: $overlay_url"
 897        cd "$dir" || die "cannot cd to overlay repo cache: $dir"
 898        git fetch --quiet origin || die "overlay fetch failed: $overlay_url"
 899        local ref
 900        if git show-ref --verify --quiet refs/remotes/origin/main; then
 901            ref="origin/main"
 902        elif git show-ref --verify --quiet refs/remotes/origin/master; then
 903            ref="origin/master"
 904        else
 905            ref="origin/HEAD"
 906        fi
 907        git reset --hard "$ref" >/dev/null 2>&1 || die "overlay reset failed: $overlay_url"
 908    else
 909        rm -rf "$dir"
 910        msg "cloning overlay repo: $overlay_url"
 911        git clone --quiet "$overlay_url" "$dir" || die "failed to clone overlay repo: $overlay_url"
 912    fi
 913
 914    printf '%s\n' "$dir"
 915}
 916
 917apply_overlay() {
 918    local overlay_url="$1" dir cat port cat_name port_name target_cat target_port count
 919    [ -d "$DTR_DIR" ] || die "$DTR_DIR does not exist (run '$PROG_NAME s' first)"
 920
 921    dir=$(sync_overlay_repo "$overlay_url")
 922    count=0
 923
 924    for cat in "$dir"/*; do
 925        [ -d "$cat" ] || continue
 926        cat_name="${cat##*/}"
 927        case "$cat_name" in
 928            .*) continue ;;
 929        esac
 930
 931        for port in "$cat"/*; do
 932            [ -d "$port" ] || continue
 933            [ -f "$port/$DTR_NEW_MAKE" ] || continue
 934            port_name="${port##*/}"
 935            target_cat="$DTR_DIR/$cat_name"
 936            target_port="$target_cat/$port_name"
 937
 938            mkdir -p "$target_cat" || die "failed to create category directory: $target_cat"
 939            rm -rf "$target_port"
 940            cp -R "$port" "$target_port" || die "failed to overlay port: $cat_name/$port_name"
 941            count=$((count + 1))
 942        done
 943    done
 944
 945    msg "overlay applied from $overlay_url ($count ports)"
 946}
 947
 948overlay_add() {
 949    local overlay_url="$1" overlays_file tmp
 950    overlays_file=$(overlay_file)
 951    tmp=$(tmp_file "overlay")
 952
 953    mkdir -p "$DTR_DIR" || die "failed to create $DTR_DIR"
 954    touch "$overlays_file" || die "failed to create overlay file: $overlays_file"
 955
 956    if grep -Fxq "$overlay_url" "$overlays_file"; then
 957        return 0
 958    fi
 959
 960    cp "$overlays_file" "$tmp" 2>/dev/null || : > "$tmp"
 961    printf '%s\n' "$overlay_url" >> "$tmp"
 962    mv "$tmp" "$overlays_file" || die "failed to update overlay file: $overlays_file"
 963}
 964
 965sync_overlays() {
 966    local overlays_file line
 967    overlays_file=$(overlay_file)
 968    [ -f "$overlays_file" ] || return 0
 969
 970    msg "syncing active overlays"
 971    while IFS= read -r line; do
 972        [ -n "$line" ] || continue
 973        case "$line" in
 974            \#*) continue ;;
 975        esac
 976        apply_overlay "$line"
 977    done < "$overlays_file"
 978}
 979
 980add_overlay() {
 981    local overlay_url="$1"
 982    valid_overlay_url "$overlay_url" || die "invalid overlay repository URL: $overlay_url"
 983
 984    apply_overlay "$overlay_url"
 985    overlay_add "$overlay_url"
 986    msg "overlay registered in $(overlay_file)"
 987}
 988
 989sync_ports() {
 990    msg "syncing ports tree"
 991
 992    local current_remote
 993
 994    if [ -d "$DTR_DIR/.git" ]; then
 995        cd "$DTR_DIR" || die "cannot cd to $DTR_DIR"
 996        current_remote=$(git config "remote.$DTR_REMOTE.url" 2>/dev/null || true)
 997
 998        if [ -n "$current_remote" ] &&
 999           [ "$current_remote" != "$DEFAULT_REPO" ] &&
1000           [ "$current_remote" != "${DEFAULT_REPO}.git" ]; then
1001            warn "current ports tree points to unexpected remote: $current_remote"
1002            info "expected: $DEFAULT_REPO"
1003            confirm_action "proceed with fetch/reset anyway?" || { msg "sync aborted"; return 1; }
1004        fi
1005
1006        msg "fetching and resetting existing repository"
1007        git fetch --quiet "$DTR_REMOTE" || die "git fetch failed"
1008        local ref
1009        ref=$(get_remote_branch)
1010        git reset --hard "$ref" || die "git reset failed"
1011    else
1012        if [ -d "$DTR_DIR" ]; then
1013            warn "directory $DTR_DIR exists but is not a git repository"
1014            confirm_action "remove it and clone fresh repository?" || { msg "sync aborted"; return 1; }
1015            rm -rf "$DTR_DIR" || die "failed to remove existing directory"
1016        fi
1017
1018        msg "cloning fresh ports tree from $DEFAULT_REPO"
1019        git clone --quiet "$DEFAULT_REPO" "$DTR_DIR" || die "clone failed"
1020    fi
1021
1022    if [ -f "$DTR_DIR/dtr" ]; then
1023        cp -f "$DTR_DIR/dtr" /bin/dtr || die "failed to copy $DTR_DIR/dtr to /bin/dtr"
1024    else
1025        warn "missing $DTR_DIR/dtr; skipping /bin/dtr update"
1026    fi
1027
1028    if [ -f "$DTR_DIR/spc" ]; then
1029        cp -f "$DTR_DIR/spc" /bin/spc || die "failed to copy $DTR_DIR/spc to /bin/spc"
1030    else
1031        warn "missing $DTR_DIR/spc; skipping /bin/spc update"
1032    fi
1033
1034    sync_overlays
1035
1036    msg "ports tree synced"
1037}
1038check_package_update() {
1039    local pkg="$1" outfile="$2" ver_new ver_old rel_new rel_old
1040
1041    ver_new=$(get_package_version "$pkg")
1042    ver_new=$(resolve_git_version "$ver_new" "$pkg")
1043    rel_new=$(get_package_release "$pkg")
1044
1045    # We query spc info for installed details
1046    if is_installed "$pkg"; then
1047        ver_old=$(spc info "$pkg" | grep "^version:" | cut -d: -f2- | sed 's/^ *//')
1048        rel_old=$(spc info "$pkg" | grep "^release:" | cut -d: -f2- | sed 's/^ *//')
1049        rel_old=${rel_old:-1}
1050    else
1051        ver_old=""
1052        rel_old=""
1053    fi
1054
1055    if [ -n "$ver_new" ] && [ -n "$ver_old" ] && [ "$ver_new" != "$ver_old" ]; then
1056        printf '%s|version|%s|%s\n' "$pkg" "$ver_old" "$ver_new" >> "$outfile"
1057    elif [ -n "$ver_new" ] && [ -n "$ver_old" ] &&
1058         [ -n "$rel_new" ] && [ -n "$rel_old" ] && [ "$rel_new" != "$rel_old" ]; then
1059        printf '%s|release|%s|%s\n' "$pkg" "$rel_old" "$rel_new" >> "$outfile"
1060    fi
1061}
1062do_upgrade() {
1063    spc list >/dev/null 2>&1 || { msg "no packages installed"; return 0; }
1064    msg "checking for updates"
1065    local upg; upg=$(tmp_file "upgrade"); > "$upg"
1066    local targets; [ $# -gt 0 ] && targets="$@" || targets=$(spc list | cut -d'+' -f1)
1067    for p in $targets; do [ -n "$p" ] || continue; check_package_update "$p" "$upg" & done; wait
1068    if [ ! -s "$upg" ]; then msg "all packages up to date"; rm -f "$upg"; return 0; fi; sort -o "$upg" "$upg"
1069    
1070    $log_printf '\n<bold>build plan for upgrade:</bold>\n'
1071    _DTR_UPGRADE_IN_PROGRESS=1 print_dep_tree "" " " 1 "" "upg" "$upg"
1072    
1073    local scope="link"
1074    if [ "$DTR_SKIP_CONFIRM" != "1" ]; then
1075        $log_printf '\n<bold>rebuild scope:</bold>\n  [1] link-time dependents\n  [2] all dependents\n  [3] target package only\n  [4] exit\nselect option [1]: '
1076        local choice; read -r choice </dev/tty; [ -z "$choice" ] && choice=1
1077        case "$choice" in 2) scope="" ;; 3) scope="none" ;; 4) rm -f "$upg"; return 0 ;; *) scope="link" ;; esac
1078    fi
1079
1080    while IFS='|' read -r pkg t o n; do
1081        msg "upgrading $pkg ($t)"
1082        DTR_SKIP_CONFIRM=1 do_make "$pkg" && install_package "$pkg" && clean_package "$pkg"
1083        [ "$scope" != "none" ] && rebuild_dependents "$pkg" "$scope" >/dev/null
1084    done < "$upg"; rm -f "$upg"
1085}
1086
1087# === LISTING & SEARCH ===
1088# laf: List all from repo, enriched with spc data
1089list_all_full() {
1090    $log_printf 'available packages in %s:\n' "$DTR_DIR"
1091
1092    # Search for ndmake.sh only
1093    find "$DTR_DIR" -mindepth 3 -maxdepth 3 -type f -name "$DTR_NEW_MAKE" -print |
1094        for_each_line dirname | for_each_line basename | sort -u |
1095        while read -r pkg; do
1096            if is_installed "$pkg"; then
1097                 $log_printf '  [I] '
1098            else
1099                 $log_printf '  [ ] '
1100            fi
1101            format_package_info "$pkg" '$pkg$ver$maint'
1102        done
1103}
1104
1105# lif: List installed via spc passthrough
1106list_installed_full() {
1107    spc list
1108}
1109
1110list_deps() {
1111    local pkg="$1" dir depsfile dep name type
1112
1113    dir=$(find_package "$pkg") || die "package $pkg not found"
1114    depsfile="$dir/deps"
1115
1116    $log_printf 'dependencies for %s (/ = link-time, > = runtime, . = build-time):\n' "$pkg"
1117
1118    if [ -f "$depsfile" ] && grep -qv '^\s*#' "$depsfile" 2>/dev/null; then
1119        read_deps_file "$depsfile" | while IFS= read -r dep; do
1120            name=$(parse_dep_name "$dep")
1121            type=$(parse_dep_type "$dep")
1122            case "$type" in
1123                link)    $log_printf '  /%s\n' "$name" ;;
1124                runtime) $log_printf '  >%s\n' "$name" ;;
1125                build)   $log_printf '  .%s\n' "$name" ;;
1126            esac
1127        done
1128    else
1129        $log_printf '  (none)\n'
1130    fi
1131}
1132
1133list_orphans() {
1134    spc orphans
1135}
1136search_packages() {
1137    local query="$1"
1138    [ -z "$query" ] && die "search query required"
1139
1140    msg "searching for: $query"
1141
1142    find "$DTR_DIR" -mindepth 3 -maxdepth 3 -type f -name "info" |
1143    while read -r infofile; do
1144        if grep -Ei "^(name|description):.*$query" "$infofile" >/dev/null; then
1145             local pkgdir
1146             pkgdir=$(dirname "$infofile")
1147             $log_printf '  '
1148             format_package_info "${pkgdir##*/}" '$pkg$desc'
1149        fi
1150    done
1151}
1152
1153# === INFO MANAGEMENT ===
1154show_info() {
1155    local pkg="$1" dir infofile
1156
1157    dir=$(find_package "$pkg") || die "package $pkg not found"
1158    infofile="$dir/info"
1159    [ -f "$infofile" ] || die "info file not found for $pkg"
1160
1161    cat "$infofile"
1162    $log_printf '\n'
1163}
1164populate_info() {
1165    [ -d "$DTR_DIR/.git" ] || die "$DTR_DIR is not a git repository"
1166
1167    msg "populating info fields from git history"
1168    local authors_tmp
1169    authors_tmp=$(tmp_file "authors")
1170
1171    find "$DTR_DIR" -mindepth 3 -maxdepth 3 -type f -name "$DTR_NEW_MAKE" |
1172    while read -r makefile; do
1173        local pkgdir pkgname infofile creator_raw maintainer_md contributors_md
1174        local name desc license
1175        pkgdir=$(dirname "$makefile")
1176        pkgname="${pkgdir##*/}"
1177        infofile="$pkgdir/info"
1178
1179        cd "$DTR_DIR" || continue
1180
1181        # Read existing fields
1182        if [ -f "$infofile" ]; then
1183            name=$(get_info_field "$infofile" "name")
1184            desc=$(get_info_field "$infofile" "description")
1185            license=$(get_info_field "$infofile" "license")
1186        fi
1187
1188        # Determine creator (first committer to build script)
1189        creator_raw=$(git log --reverse --format='%an <%ae>' -- "$makefile" | head -n 1)
1190        [ -z "$creator_raw" ] && continue
1191
1192        # Get all contributors sorted by commit count
1193        git log --format='%an <%ae>' -- "$makefile" |
1194            sort | uniq -c | sort -rn > "$authors_tmp"
1195
1196        # Build markdown strings (sanitize names: spaces -> underscores)
1197        maintainer_md=$(echo "$creator_raw" | sed 's/^\(.*\) <\(.*\)>$/[\1](\2)/' | sed 's/\[\([^]]*\)\]/[\1]/' | sed 's/\[\([^]]*\) /[\1_/g' | sed 's/ \([^]]*\)\]/_\1]/g')
1198        contributors_md=""
1199
1200        # Filter creator from contributors, format others
1201        local contrib_list
1202        contrib_list=$(grep -vF "$creator_raw" "$authors_tmp" | sed -E 's/^[[:space:]]*[0-9]+[[:space:]]+//')
1203
1204        if [ -n "$contrib_list" ]; then
1205            contributors_md=$(echo "$contrib_list" | sed 's/^\(.*\) <\(.*\)>$/[\1](\2)/' | sed 's/\[\([^]]*\)\]/[\1]/' | sed 's/\[\([^]]*\) /[\1_/g' | sed 's/ \([^]]*\)\]/_\1]/g' | paste -sd ',' - | sed 's/,/, /g')
1206        fi
1207
1208        # Update info file (create if doesn't exist)
1209        if [ ! -f "$infofile" ]; then
1210            touch "$infofile"
1211        fi
1212
1213        # Remove existing maintainer/contributors fields
1214        sed -i '/^maintainer:/d' "$infofile"
1215        sed -i '/^contributors:/d' "$infofile"
1216
1217        # Set proper name if empty
1218        if [ -z "$name" ]; then
1219            printf 'name: %s\n' "$pkgname" >> "$infofile"
1220        fi
1221
1222        # Warn if description is empty
1223        [ -z "$desc" ] && warn "package $pkgname: description field is empty"
1224
1225        # Warn if license is empty
1226        [ -z "$license" ] && warn "package $pkgname: license field is empty"
1227
1228        # Append maintainer and contributors
1229        printf 'maintainer: %s\n' "$maintainer_md" >> "$infofile"
1230        if [ -n "$contributors_md" ]; then
1231            printf 'contributors: %s\n' "$contributors_md" >> "$infofile"
1232        fi
1233
1234        info "updated: $pkgname"
1235    done
1236
1237    rm -f "$authors_tmp"
1238    msg "info population complete"
1239}
1240generate_dmake_scripts() {
1241    local dir="${1:-$DTR_DIR}"
1242    [ -d "$dir" ] || die "directory $dir does not exist"
1243
1244    find "$dir" -mindepth 1 -maxdepth 1 -type d |
1245    while read -r d; do
1246        local makefile pkgname
1247        makefile="$d/$DTR_NEW_MAKE"
1248        [ -f "$makefile" ] && continue
1249
1250        pkgname="${d##*/}"
1251        msg "creating $DTR_NEW_MAKE for $pkgname"
1252
1253        cat > "$makefile" <<'EOF'
1254#!/bin/sh
1255# ndmake.sh template for $pkgname
1256
1257case "${1:-}" in
1258    make)    ;;
1259    install) ;;
1260    clean)   ;;
1261    remove)  ;;
1262    *) echo "usage: $0 {make|install|clean|remove}"; exit 1 ;;
1263esac
1264EOF
1265        chmod +x "$makefile"
1266    done
1267}
1268
1269# === EXTENSIONS ===
1270find_extension() {
1271    local ext="$1" dir
1272
1273    for dir in $EXTENSION_DIRS; do
1274        if [ -d "$dir" ] && [ -f "$dir/$ext" ]; then
1275            printf '%s' "$dir/$ext"
1276            return 0
1277        fi
1278    done
1279    return 1
1280}
1281
1282call_extension() {
1283    local ext="$1" ext_path
1284    shift
1285
1286    ext_path=$(find_extension "$ext") || return 1
1287    sh "$ext_path" "$@"
1288}
1289process_chain() {
1290    local chain="$1" i=0 c next_c
1291
1292    while [ $i -lt ${#chain} ]; do
1293        c=$(printf '%s' "$chain" | cut -c$((i + 1)))
1294        i=$((i + 1))
1295
1296        case "$c" in
1297            m) do_make "$pkg" || return ;;
1298            d) do_make_nodeps "$pkg" || return ;;
1299            i) install_package "$pkg" || return ;;
1300            c) clean_package "$pkg" || return ;;
1301            r) remove_package "$pkg" || return ;;
1302            p) purge_package "$pkg" || return ;;
1303            s) sync_ports || return ;;
1304            u) do_upgrade || return ;;
1305            o) add_overlay "$pkg" || return ;;
1306            g) generate_dmake_scripts "$pkg" || return ;;
1307            f) show_info "$pkg" || return ;;
1308            l)
1309                next_c=$(printf '%s' "$chain" | cut -c$((i + 1)))
1310                case "$next_c" in
1311                    a) list_all_full; return ;;
1312                    i) list_installed_full; return ;;
1313                    d) [ -n "$pkg" ] && list_deps "$pkg"; return ;;
1314                    o) list_orphans; return ;;
1315                    *) die "invalid list command: l$next_c (use la, li, ld, or lo)" ;;
1316                esac
1317                i=$((i + 1))
1318                ;;
1319            *)
1320                call_extension "$c" "$pkg" || die "unknown command character: $c (no '$c' extension found)"
1321                ;;
1322        esac
1323    done
1324}
1325
1326# === MAIN ===
1327main() {
1328    if [ $# -eq 0 ]; then
1329        cat <<EOF
1330usage: $PROG_NAME [y] <cmd> <pkgs>
1331       $PROG_NAME m|i|c|r|p|d|g <pkgs>
1332       $PROG_NAME o|overlay <overlay-git-repo>
1333       $PROG_NAME la|li|laf|lif|ld|lo|sp|f <pkgs>
1334       $PROG_NAME s|u|pi|se|ar|lo
1335       $PROG_NAME mic|dic <pkgs>  (build+install+clean; d=no deps)
1336EOF
1337        exit 0
1338    fi
1339
1340    case "${1:-}" in
1341        y|-y|--yes)
1342            DTR_SKIP_CONFIRM=1
1343            shift
1344            ;;
1345    esac
1346
1347    local cmd="$1"
1348    case "$cmd" in
1349        y*)
1350            DTR_SKIP_CONFIRM=1
1351            cmd="${cmd#y}"
1352            ;;
1353    esac
1354    shift
1355
1356    case "$cmd" in
1357        chill)
1358            chill "$0" && exit 0
1359            ;;
1360        laf|la)
1361            list_all_full && exit 0
1362            ;;
1363        lif|li)
1364            list_installed_full && exit 0
1365            ;;
1366        lo)
1367            list_orphans && exit 0
1368            ;;
1369        s|sync)
1370            sync_ports && exit 0
1371            ;;
1372        u|upgrade)
1373            do_upgrade && exit 0
1374            ;;
1375        ar|autoremove)
1376            spc autoremove && exit 0
1377            ;;
1378        pi|populate-info)
1379            populate_info && exit 0
1380            ;;
1381        se|search)
1382            [ $# -eq 0 ] && die "search query required"
1383            search_packages "$*" && exit 0
1384            ;;
1385        o|overlay)
1386            [ $# -eq 0 ] && die "overlay repository URL required"
1387            add_overlay "$1" && exit 0
1388            ;;
1389    esac
1390
1391    # Check if command is valid before requiring package
1392    case "$cmd" in
1393        ld|sp|f|g|gen|m|make|i|install|c|clean|r|remove|p|purge|d|o|overlay)
1394            [ $# -eq 0 ] && die "package required for '$cmd'"
1395            ;;
1396        *)
1397            # Check if it's an extension or valid chain command
1398            local ext_path is_valid=0
1399            ext_path=$(find_extension "$cmd") && is_valid=1
1400
1401            # Check if it's a valid chain (only contains valid command chars)
1402            if [ "$is_valid" -eq 0 ]; then
1403                local i=0 c
1404                while [ $i -lt ${#cmd} ]; do
1405                    c=$(printf '%s' "$cmd" | cut -c$((i + 1)))
1406                    case "$c" in
1407                        m|i|c|r|p|s|u|g|f|l|d|o) is_valid=1 ;;
1408                        *)
1409                            # Could still be an extension for this char
1410                            find_extension "$c" >/dev/null 2>&1 && is_valid=1 || is_valid=0
1411                            [ "$is_valid" -eq 0 ] && break
1412                            ;;
1413                    esac
1414                    i=$((i + 1))
1415                done
1416            fi
1417
1418            if [ "$is_valid" -eq 0 ]; then
1419                die "invalid command: '$cmd'"
1420            fi
1421
1422            [ $# -eq 0 ] && die "package required for '$cmd'"
1423            ;;
1424    esac
1425
1426    for pkg in "$@"; do
1427        pkg=$(resolve_package_arg "$pkg")
1428
1429        case "$cmd" in
1430            ld)
1431                list_deps "$pkg"
1432                ;;
1433            sp)
1434                local found
1435                found=$(find_package "$pkg")
1436                if [ -n "$found" ]; then
1437                    printf '%s\n' "$found"
1438                else
1439                    printf 'not found\n'
1440                fi
1441                ;;
1442            f)
1443                show_info "$pkg"
1444                ;;
1445            g|gen)
1446                generate_dmake_scripts "$pkg"
1447                ;;
1448            m|make)
1449                do_make "$pkg"
1450                ;;
1451            i|install)
1452                install_package "$pkg"
1453                ;;
1454            c|clean)
1455                clean_package "$pkg"
1456                ;;
1457            r|remove)
1458                remove_package "$pkg"
1459                ;;
1460            p|purge)
1461                purge_package "$pkg"
1462                ;;
1463            d)
1464                do_make_nodeps "$pkg"
1465                ;;
1466            *)
1467                local ext_path
1468                ext_path=$(find_extension "$cmd")
1469                if [ -n "$ext_path" ]; then
1470                    "$ext_path" "$pkg"
1471                else
1472                    process_chain "$cmd"
1473                fi
1474                ;;
1475        esac
1476    done
1477
1478    exit 0
1479}
1480
1481main "$@"