diff options
Diffstat (limited to 'bin/shuriken')
| -rwxr-xr-x | bin/shuriken | 164 |
1 files changed, 129 insertions, 35 deletions
diff --git a/bin/shuriken b/bin/shuriken index a475a3a..5b8f974 100755 --- a/bin/shuriken +++ b/bin/shuriken @@ -115,37 +115,113 @@ log_warning() { } # Inlined from src/lib/compat.source.sh -# Runtime compatibility guard: verify the standard Unix tools shuriken shells -# out to are the GNU variants. shuriken relies on GNU-only extensions -- find -# -printf, stat -c, cp -a, and sort -R -- that the BSD tools shipped with macOS -# and BSD systems do not provide (see the "Platform compatibility" section of -# README.md). require_gnu_tools probes each feature in a throwaway temp dir and -# exits with a clear error naming the offending tool if any probe fails, so the -# check runs before any real work begins (it is called from main, after CLI -# parsing and before run_action). +# Cross-platform tool resolution and runtime compatibility guard. # -# The probes intentionally exercise the exact GNU-only behavior the codebase -# depends on rather than parsing --help text (which is unstable across -# implementations), so a tool that lacks the feature fails the probe regardless -# of its version string. +# shuriken relies on GNU-only extensions of four standard Unix tools -- find +# -printf, stat -c, cp -a, and sort -R (see the "Platform compatibility" +# section of README.md). Those extensions are not supported by the BSD +# variants of the same tools shipped with macOS and FreeBSD. Rather than being +# Linux-only, shuriken resolves each of the four tools to a variable (FIND, +# STAT, CP, SORT) once at startup: on Linux the plain command is already GNU, +# and on macOS/FreeBSD the GNU version is normally installed side-by-side +# under a "g" prefix (gfind, gstat, gcp, gsort, via Homebrew/pkg -- see +# README.md) to avoid clobbering the system tools. This mirrors the +# $SED/$GREP/$DATE tool-selection variables in the sibling gemtexter project +# (github.com/snonux/gemtexter). Every call site elsewhere in the codebase +# that depends on the GNU-only behavior uses these variables instead of the +# bare command name; call sites that only need POSIX-portable behavior are +# left as plain "find"/"sort"/etc and are unaffected by any of this. +# +# require_gnu_tools (called from main, after CLI parsing and before +# run_action) verifies the resolved tools are genuinely GNU before any real +# work begins, so a misconfigured host fails fast with a clear error instead +# of failing confusingly deep inside generation. + +# Resolve the binary name for a single bare coreutils/findutils tool name: +# the GNU-prefixed variant (e.g. "gfind" for "find") if one is on PATH, +# otherwise the bare name unchanged. On Linux a "g"-prefixed sibling is not +# normally installed, so this resolves to the bare (already-GNU) name; on +# macOS/FreeBSD it picks up the Homebrew/pkg-installed GNU version. No uname +# check is needed: preferring the g-prefixed binary when present is correct +# on every platform. +resolve_gnu_tool() { + local -r bare_name="$1" + + if command -v "g$bare_name" >/dev/null 2>&1; then + printf '%s\n' "g$bare_name" + else + printf '%s\n' "$bare_name" + fi +} + +FIND=$(resolve_gnu_tool find) +STAT=$(resolve_gnu_tool stat) +CP=$(resolve_gnu_tool cp) +SORT=$(resolve_gnu_tool sort) +readonly FIND STAT CP SORT + +# Quick preflight: confirm each resolved tool self-reports as GNU before +# running the (slower) behavioral probes in require_gnu_tools. This gives a +# fast, specific error on a genuine BSD/macOS host that has no g-prefixed +# tools installed at all, naming exactly which tool and pointing at the +# README install instructions, rather than only failing on the deeper +# behavioral probe below. A tool whose --version output happens to lie (or +# omit a version string) still falls through to the behavioral probe, which +# is authoritative. +verify_gnu_tool_versions() { + local -r tool="$1" + local -r label="$2" + local version_output + + version_output=$("$tool" --version 2>/dev/null || true) + if [[ "$version_output" != *GNU* ]]; then + printf 'ERROR: "%s" (%s) does not report itself as GNU.\n' \ + "$tool" "$label" >&2 + printf 'ERROR: Install GNU coreutils/findutils -- see the\n' >&2 + printf 'ERROR: "Platform compatibility" section of README.md.\n' >&2 + return 1 + fi +} +# Runtime compatibility guard: verify the resolved tools (FIND/STAT/CP/SORT) +# actually behave like the GNU variants shuriken depends on. Each probe +# exercises the exact GNU-only behavior the codebase relies on (rather than +# parsing --help/--version text, which is unstable across implementations), +# so a tool that merely claims to be GNU but lacks the feature still fails +# here. Called from main, after CLI parsing and before run_action, so the +# check runs before any real work begins. require_gnu_tools() { local probe_dir - local probe_out - local failed='' + + verify_gnu_tool_versions "$FIND" find || return 1 + verify_gnu_tool_versions "$STAT" stat || return 1 + verify_gnu_tool_versions "$CP" cp || return 1 + verify_gnu_tool_versions "$SORT" sort || return 1 probe_dir=$(mktemp -d 2>/dev/null) || return 1 # shellcheck disable=SC2064 # expand probe_dir now, clean up on any return trap "rm -rf '$probe_dir'" RETURN + probe_gnu_tool_behavior "$probe_dir" +} + +# The behavioral half of require_gnu_tools, split out to keep each function +# under ~30 lines (per repo convention): probes FIND/STAT/CP/SORT's actual +# GNU-only behavior in a throwaway temp dir and reports via +# report_gnu_tool_failure on the first failure. +probe_gnu_tool_behavior() { + local -r probe_dir="$1"; shift + local probe_out + local failed='' + # GNU find supports the -printf action; BSD/macOS find does not. - probe_out=$(find "$probe_dir" -maxdepth 0 -printf '%f\n' 2>/dev/null) \ + probe_out=$("$FIND" "$probe_dir" -maxdepth 0 -printf '%f\n' 2>/dev/null) \ && [ -n "$probe_out" ] || failed='find (missing the -printf action)' # GNU stat uses -c FORMAT; BSD/macOS stat uses -f and rejects -c. if [ -z "$failed" ]; then printf 'probe\n' > "$probe_dir/file" - probe_out=$(stat -c '%s' "$probe_dir/file" 2>/dev/null) \ + probe_out=$("$STAT" -c '%s' "$probe_dir/file" 2>/dev/null) \ && [[ "$probe_out" =~ ^[0-9]+$ ]] \ || failed='stat (missing the -c option)' fi @@ -154,7 +230,7 @@ require_gnu_tools() { if [ -z "$failed" ]; then mkdir "$probe_dir/src" printf 'x\n' > "$probe_dir/src/inner" - if ! cp -a "$probe_dir/src" "$probe_dir/dest" 2>/dev/null \ + if ! "$CP" -a "$probe_dir/src" "$probe_dir/dest" 2>/dev/null \ || [ ! -f "$probe_dir/dest/inner" ]; then failed='cp (missing the -a option)' fi @@ -162,19 +238,25 @@ require_gnu_tools() { # GNU sort supports -R (random shuffle); BSD sort lacks it. if [ -z "$failed" ]; then - probe_out=$(printf 'a\nb\nc\n' | sort -R 2>/dev/null) \ + probe_out=$(printf 'a\nb\nc\n' | "$SORT" -R 2>/dev/null) \ && [ -n "$probe_out" ] || failed='sort (missing the -R option)' fi - if [ -n "$failed" ]; then - printf 'ERROR: shuriken requires the GNU versions of the standard Unix\n' >&2 - printf 'ERROR: tools (GNU coreutils/findutils). Non-GNU or unsupported: %s\n' \ - "$failed" >&2 - printf 'ERROR: shuriken is Linux-only and will not run on macOS or BSD.\n' >&2 - return 1 - fi + [ -z "$failed" ] || report_gnu_tool_failure "$failed" +} - return 0 +# Shared error reporter for require_gnu_tools/probe_gnu_tool_behavior: prints +# the clear "install GNU tools" message naming the offending tool/feature and +# returns 1 (never exits directly, so callers stay in control of unwinding). +report_gnu_tool_failure() { + local -r failed="$1"; shift + + printf 'ERROR: shuriken requires the GNU versions of the standard Unix\n' >&2 + printf 'ERROR: tools (GNU coreutils/findutils). Non-GNU or unsupported: %s\n' \ + "$failed" >&2 + printf 'ERROR: On macOS/FreeBSD, install GNU coreutils/findutils (see the\n' >&2 + printf 'ERROR: "Platform compatibility" section of README.md) and retry.\n' >&2 + return 1 } # Inlined from src/lib/bootstrap.source.sh @@ -1741,11 +1823,14 @@ is_supported_image_file() { incoming_image_files() { local file + # $FIND (compat.source.sh): -printf is a GNU-only action, so this must run + # against the resolved GNU find, not whatever "find" plain resolves to on + # macOS/FreeBSD. The trailing sort is POSIX-portable and left as-is. while IFS= read -r file; do if is_supported_image_file "$file"; then printf '%s\n' "$file" fi - done < <(find "$INCOMING_DIR" -maxdepth 1 -type f -printf '%f\n') \ + done < <("$FIND" "$INCOMING_DIR" -maxdepth 1 -type f -printf '%f\n') \ | sort } @@ -1790,11 +1875,12 @@ count_tree_files() { warn_unsupported_incoming_files() { local file + # $FIND (compat.source.sh): -printf is a GNU-only action. while IFS= read -r file; do if ! is_supported_image_file "$file"; then log_warning "Ignoring unsupported incoming file: $file" fi - done < <(find "$INCOMING_DIR" -maxdepth 1 -type f -printf '%f\n' | sort) + done < <("$FIND" "$INCOMING_DIR" -maxdepth 1 -type f -printf '%f\n' | sort) } scalephotos() { @@ -1912,9 +1998,11 @@ maybe_shuffle() { if random_seed_is_set; then deterministic_shuffle else - sort -R + # $SORT (compat.source.sh): -R (random shuffle) is GNU-only. + "$SORT" -R fi else + # Plain sort here is POSIX-portable; no GNU-only flag involved. sort fi } @@ -1945,11 +2033,13 @@ maybe_shuffle() { # suppressed (the stats background loader) adds its own 2>/dev/null. This is the # SORTED listing; album_photo_files deliberately keeps its own maybe_shuffle # variant because the album's display order is the configurable shuffle, not a -# plain sort. +# plain sort. Uses $FIND (compat.source.sh): -printf is GNU-only, so this must +# run against the resolved GNU find, not whatever "find" plain resolves to on +# macOS/FreeBSD. The trailing plain "sort" is POSIX-portable and left as-is. list_photos() { local -r photos_dir="$1"; shift - find "$DIST_DIR/$photos_dir" -maxdepth 1 -type f -printf '%f\n' | sort + "$FIND" "$DIST_DIR/$photos_dir" -maxdepth 1 -type f -printf '%f\n' | sort } # Pick one seeded-random entry from an already-collected list, addressed by @@ -2063,12 +2153,14 @@ exif_cache_dir() { # Build the cache signature line ("<photo>:<size>:<mtime>") used to decide # whether a cache entry is still valid for the source file. Kept private to this # module alongside its only consumers, plus the stats test that pre-seeds caches. +# Uses $STAT (compat.source.sh): -c FORMAT is GNU-only (BSD/macOS stat uses -f +# and rejects -c), so this must run against the resolved GNU stat. photo_cache_signature() { local -r photo="$1"; shift local -r photo_path="$1"; shift local stat_output - stat_output=$(stat -c '%s:%Y' "$photo_path") + stat_output=$("$STAT" -c '%s:%Y' "$photo_path") printf '%s:%s\n' "$photo" "$stat_output" } @@ -3416,11 +3508,11 @@ build_preview_thumbnail() { # Unlike the other photo listings this one keeps its own find rather than using # list_photos (photo-list.source.sh): it pipes through maybe_shuffle, not sort, # because the album's display order is the configurable (seeded) shuffle, not a -# plain sort. +# plain sort. Uses $FIND (compat.source.sh) since -printf is a GNU-only action. album_photo_files() { local -r photos_dir="$1"; shift - find "$DIST_DIR/$photos_dir" -maxdepth 1 -type f -printf '%f\n' \ + "$FIND" "$DIST_DIR/$photos_dir" -maxdepth 1 -type f -printf '%f\n' \ | maybe_shuffle } @@ -6195,12 +6287,14 @@ prepare_generation_staging_dir() { return fi + # $CP (compat.source.sh): -a (archive mode) is not supported by minimal/older + # BSD cp variants, so this must run against the resolved GNU cp. for cache_dir in photos thumbs blurs; do if [ -d "$final_dist/$cache_dir" ]; then if ! mkdir -p "$staging_dir/$cache_dir"; then return 1 fi - if ! cp -a "$final_dist/$cache_dir/." "$staging_dir/$cache_dir/"; then + if ! "$CP" -a "$final_dist/$cache_dir/." "$staging_dir/$cache_dir/"; then return 1 fi fi |
