summaryrefslogtreecommitdiff
path: root/src/lib/compat.source.sh
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/compat.source.sh')
-rw-r--r--src/lib/compat.source.sh134
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
}