diff options
Diffstat (limited to 'src/lib/compat.source.sh')
| -rw-r--r-- | src/lib/compat.source.sh | 134 |
1 files changed, 108 insertions, 26 deletions
diff --git a/src/lib/compat.source.sh b/src/lib/compat.source.sh index 718c7ed..63111fd 100644 --- a/src/lib/compat.source.sh +++ b/src/lib/compat.source.sh @@ -1,34 +1,110 @@ -# 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 @@ -37,7 +113,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 @@ -45,17 +121,23 @@ 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" +} + +# 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 - return 0 + 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 } |
