graft

simple package manager
git clone git://git.rr3.xyz/graft | git clone gits://git.rr3.xyz/graft
Log | Files | Refs | README | LICENSE

graft (5443B)


      1 #!/bin/bash
      2 set -euo pipefail
      3 
      4 USAGE="$(
      5 cat <<EOF
      6 Usage: $(basename "$0") [-n] ROOT PKGS...
      7 EOF
      8 )"
      9 readonly USAGE
     10 
     11 # ------------------------------------------------------------------------------
     12 # Utility
     13 
     14 echo_lines() {
     15 	[ $# -ge 1 ] && printf "%s\n" "$@"
     16 }
     17 
     18 fail() {
     19 	echo_lines "$@" >&2
     20 	exit 1
     21 }
     22 
     23 # Resolve a path to an absolute path without dereferencing symlinks.
     24 abspath() { realpath -ms "$@"; }
     25 
     26 fg_color_code() {
     27 	case "$1" in
     28 	default) echo 39 ;;
     29 
     30 	black)   echo 30 ;;
     31 	red)     echo 31 ;;
     32 	green)   echo 32 ;;
     33 	yellow)  echo 33 ;;
     34 	blue)    echo 34 ;;
     35 	magenta) echo 35 ;;
     36 	cyan)    echo 36 ;;
     37 	white)   echo 37 ;;
     38 
     39 	bright-black)   echo 90 ;;
     40 	bright-red)     echo 91 ;;
     41 	bright-green)   echo 92 ;;
     42 	bright-yellow)  echo 93 ;;
     43 	bright-blue)    echo 94 ;;
     44 	bright-magenta) echo 95 ;;
     45 	bright-cyan)    echo 96 ;;
     46 	bright-white)   echo 97 ;;
     47 	esac
     48 }
     49 
     50 ansi_style() {
     51 	if [ -n "$USE_COLOR" ]; then
     52 		echo -en "\e[${1}m"
     53 	fi
     54 }
     55 ansi_reset() { ansi_style ""; }
     56 
     57 start_color() { ansi_style "$(fg_color_code "$1")"; }
     58 stop_color() { ansi_style 39; }
     59 
     60 start_bold() { ansi_style 1; }
     61 stop_bold() { ansi_style 22; }
     62 
     63 # ------------------------------------------------------------------------------
     64 # Package API
     65 
     66 msg() {
     67 	[ $# -ge 3 ] || fail "Usage: msg COLOR LABEL LINES..."
     68 	local color; color="$1"
     69 	local label; label="$2"
     70 	local first; first="$3"
     71 	shift 3
     72 
     73 	local prefix; prefix="$(stop_color)[$NAME] $(start_color "$color")"
     74 	echo "$prefix$(start_bold)$label$(stop_bold)$first"
     75 	echo_lines "${@/#/$prefix$(printf '%*s' ${#label} '')}"
     76 	stop_color
     77 }
     78 
     79 msg_note() { msg magenta "NOTE: " "$@"; }
     80 msg_info() { msg default "INFO: " "$@"; }
     81 msg_warning() { msg yellow "WARNING: " "$@"; }
     82 msg_error() { msg red "ERROR: " "$@"; }
     83 msg_fatal() {
     84 	msg bright-red "FATAL: " "$@" >&2
     85 	exit 1
     86 }
     87 msg_bad_package() {
     88 	[ $# -ge 1 ] || fail "Usage: msg_bad_package REASON CONTEXT..."
     89 	local reason; reason="$1"
     90 	shift 1
     91 	msg_fatal "bad package: $reason" "$@"
     92 }
     93 
     94 link() {
     95 	[ $# -eq 2 ] || [ $# -eq 1 ] || msg_bad_package "invalid link" "Usage: link DST_PATH SRC_PATH | link PATH"
     96 	local rel_dst; rel_dst="$1"
     97 	local rel_src; rel_src="${2:-$1}"
     98 
     99 	local abs_dst; abs_dst="$ROOT/$rel_dst"
    100 	local abs_src; abs_src="$PKG/$rel_src"
    101 
    102 	[ -e "$abs_src" ] || [ -h "$abs_src" ] || msg_bad_package "invalid link: source does not exist"
    103 
    104 	local move; move=""
    105 	if [ -e "$abs_dst" ] || [ -h "$abs_dst" ]; then
    106 		case "$(readlink -q "$abs_dst")" in
    107 		"$abs_src")
    108 			msg bright-black "LINK OK: " "$abs_dst --> $rel_src"
    109 			return 0
    110 			;;
    111 		"$PKG"/*)
    112 			move=1
    113 			;;
    114 		*)
    115 			msg red "LINK CONFLICT: " "$abs_dst -/-> $rel_src"
    116 			return 1
    117 			;;
    118 		esac
    119 	fi
    120 
    121 	if [ -z "$DRY" ]; then
    122 		local err
    123 
    124 		local parent; parent="$(dirname "$abs_dst")"
    125 		if ! err="$(mkdir -p "$parent" 2>&1)"; then
    126 			msg red "LINK FAILED: " "$abs_dst -/-> $rel_src" "$err"
    127 			return 1
    128 		fi
    129 
    130 		local -a ln_opt=(); [ -n "$move" ] && ln_opt=("-f")
    131 		if ! err="$(ln -sn "${ln_opt[@]}" "$abs_src" "$abs_dst" 2>&1)"; then
    132 			msg red "LINK FAILED: " "$abs_dst -/-> $rel_src" "$err"
    133 			return 1
    134 		fi
    135 	fi
    136 
    137 	if [ -n "$move" ]; then
    138 		msg cyan "LINK MOVE: " "$abs_dst --> $rel_src"
    139 	else
    140 		msg blue "LINK CREATE: " "$abs_dst --> $rel_src"
    141 	fi
    142 }
    143 
    144 copy() {
    145 	[ $# -eq 2 ] || [ $# -eq 1 ] || msg_bad_package "invalid copy" "Usage: copy DST_PATH SRC_PATH | copy PATH"
    146 	local rel_dst; rel_dst="$1"
    147 	local rel_src; rel_src="${2:-$1}"
    148 
    149 	local abs_dst; abs_dst="$ROOT/$rel_dst"
    150 	local abs_src; abs_src="$PKG/$rel_src"
    151 
    152 	[ -e "$abs_src" ] || [ -h "$abs_src" ] || msg_bad_package "invalid copy: source does not exist"
    153 
    154 	if [ -e "$abs_dst" ] || [ -h "$abs_dst" ]; then
    155 		if diff -rq "$abs_dst" "$abs_src" >/dev/null 2>&1; then
    156 			msg bright-black "COPY OK: " "$abs_dst <-- $rel_src"
    157 			return 0
    158 		else
    159 			msg red "COPY CONFLICT: " "$abs_dst <-/- $rel_src"
    160 			return 1
    161 		fi
    162 	fi
    163 
    164 	if [ -z "$DRY" ]; then
    165 		local err
    166 
    167 		local parent; parent="$(dirname "$abs_dst")"
    168 		if ! err="$(mkdir -p "$parent" 2>&1)"; then
    169 			msg red "COPY FAILED: " "$abs_dst <-/- $rel_src" "$err"
    170 			return 1
    171 		fi
    172 
    173 		if ! err="$(cp -rpT "$abs_src" "$abs_dst" 2>&1)"; then
    174 			msg red "COPY FAILED: " "$abs_dst <-/- $rel_src" "$err"
    175 			return 1
    176 		fi
    177 	fi
    178 
    179 	msg green "COPY CREATE: " "$abs_dst <-- $rel_src"
    180 }
    181 
    182 # ------------------------------------------------------------------------------
    183 # Input Handling
    184 
    185 # Options
    186 opt_force_color=""
    187 opt_dry=""
    188 while getopts "cnh" opt; do
    189 	case "$opt" in
    190 		c) opt_force_color=1 ;;
    191 		n) opt_dry=1 ;;
    192 		h) echo "$USAGE"; exit 0 ;;
    193 		*) fail "$USAGE" ;;
    194 	esac
    195 done
    196 readonly opt_force_color
    197 readonly opt_dry
    198 shift $((OPTIND - 1))
    199 
    200 # Arguments
    201 [ $# -ge 1 ] || fail "$USAGE"
    202 arg_root="$1"; readonly arg_root
    203 shift 1
    204 arg_pkgs=("$@"); readonly arg_pkgs
    205 
    206 # ------------------------------------------------------------------------------
    207 # Globals
    208 
    209 DRY="$opt_dry"; readonly DRY
    210 ROOT="$(abspath "$arg_root")"; readonly ROOT
    211 
    212 USE_COLOR=""
    213 if [ -z "${NO_COLOR-}" ] && [ -t 1 ] || [ -n "$opt_force_color" ]; then
    214 	USE_COLOR=1
    215 fi
    216 readonly USE_COLOR
    217 
    218 # ------------------------------------------------------------------------------
    219 # Main
    220 
    221 graft() {
    222 	local NAME; NAME="$1"; readonly NAME
    223 	local PKG; PKG="$(abspath "$NAME")"; readonly PKG
    224 
    225 	[ -d "$PKG" ] || msg_bad_package "not found: $PKG"
    226 	cd "$PKG"
    227 	[ -r .graft.sh ] || msg_bad_package "could not read .graft.sh"
    228 	set +e
    229 	source ./.graft.sh
    230 	return 0
    231 }
    232 
    233 [ -d "$ROOT" ] || fail "root is not a directory"
    234 for p in "${arg_pkgs[@]}"; do
    235 	(graft "$p") || true  # TODO: Count errors?
    236 done