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 "$@"