summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-25 12:35:12 +0300
committerPaul Buetow <paul@buetow.org>2026-06-25 12:35:12 +0300
commit12b2733dab6da12a2a6cf1d27d69bc7d143f5dfc (patch)
tree077adb3e5efc5e3a0bf9f1a5b24c38ebb264deb5
parent964901d672d8e12baf4ac0eb29821d670ae15eb8 (diff)
Add runtime GNU-tool guard; document Linux-only platform support
shuriken shells out to GNU-only features of the standard Unix tools (find -printf, stat -c, cp -a, sort -R). Add require_gnu_tools in a new src/lib/compat.source.sh, sourced early and invoked from main() before any action runs. On invocation it feature-probes each tool in a throwaway temp dir; if any probe fails it prints a clear error naming the offending tool and exits 1, so non-GNU (macOS/BSD) environments fail fast instead of producing broken output. README gains a Platform compatibility section and the requirements line now mentions GNU coreutils/findutils. Tests cover the find and stat rejection paths; the shared test helper that builds a coreutils-without-imagemagick PATH now includes cp and stat (which the guard probes).
-rw-r--r--Justfile2
-rw-r--r--README.md13
-rwxr-xr-xbin/shuriken69
-rw-r--r--src/lib/compat.source.sh61
-rwxr-xr-xsrc/shuriken.sh8
-rwxr-xr-xtests/cli.sh79
-rwxr-xr-xtests/helpers.sh2
7 files changed, 233 insertions, 1 deletions
diff --git a/Justfile b/Justfile
index d7f1c79..f68fe5e 100644
--- a/Justfile
+++ b/Justfile
@@ -7,7 +7,7 @@ PREFIX := env_var_or_default("PREFIX", "/usr")
BINDIR := env_var_or_default("BINDIR", PREFIX + "/bin")
DATADIR := env_var_or_default("DATADIR", PREFIX + "/share")
SYSCONFDIR := env_var_or_default("SYSCONFDIR", "/etc/default")
-LIB_SOURCES := "src/lib/logging.source.sh src/lib/bootstrap.source.sh src/lib/paths.source.sh src/lib/imagemagick.source.sh src/lib/process.source.sh src/lib/archive.source.sh src/lib/template.source.sh src/lib/job-pool.source.sh src/lib/image.source.sh src/lib/random.source.sh src/lib/photo-list.source.sh src/lib/metadata-label.source.sh src/lib/metadata-cache.source.sh src/lib/image-pipeline.source.sh src/lib/album-metadata.source.sh src/lib/generation-metadata.source.sh src/lib/dry-run.source.sh src/lib/album-tile-layout.source.sh src/lib/album-thumbnail-html.source.sh src/lib/album-photo-select.source.sh src/lib/album-render.source.sh src/lib/album.source.sh src/lib/stats-aggregate.source.sh src/lib/stats-render.source.sh src/lib/stats-filter-album.source.sh src/lib/config.source.sh src/lib/config.print.source.sh src/lib/config.sync.source.sh src/lib/config.staging.source.sh src/lib/config.validate.source.sh src/lib/config.cli.source.sh src/lib/action.source.sh"
+LIB_SOURCES := "src/lib/logging.source.sh src/lib/compat.source.sh src/lib/bootstrap.source.sh src/lib/paths.source.sh src/lib/imagemagick.source.sh src/lib/process.source.sh src/lib/archive.source.sh src/lib/template.source.sh src/lib/job-pool.source.sh src/lib/image.source.sh src/lib/random.source.sh src/lib/photo-list.source.sh src/lib/metadata-label.source.sh src/lib/metadata-cache.source.sh src/lib/image-pipeline.source.sh src/lib/album-metadata.source.sh src/lib/generation-metadata.source.sh src/lib/dry-run.source.sh src/lib/album-tile-layout.source.sh src/lib/album-thumbnail-html.source.sh src/lib/album-photo-select.source.sh src/lib/album-render.source.sh src/lib/album.source.sh src/lib/stats-aggregate.source.sh src/lib/stats-render.source.sh src/lib/stats-filter-album.source.sh src/lib/config.source.sh src/lib/config.print.source.sh src/lib/config.sync.source.sh src/lib/config.staging.source.sh src/lib/config.validate.source.sh src/lib/config.cli.source.sh src/lib/action.source.sh"
default: build
diff --git a/README.md b/README.md
index 69c832e..ee381a3 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,16 @@ shuriken is a Bash script for Unix-like operating systems (such as Linux) that
generates static web photo albums. The resulting album is pure HTML+CSS — no
JavaScript.
+## Platform compatibility
+
+shuriken relies on **GNU** versions of the standard Unix tools. Specifically,
+it uses GNU-only extensions such as `find -printf`, `stat -c`, `cp -a`, and
+`sort -R`, which are not supported by the BSD variants shipped with macOS and
+BSD systems. As a result shuriken is currently **Linux-only** and will not run
+unmodified on macOS or BSD-based systems (including the stock tools that ship
+with macOS). If you are on macOS, run shuriken inside a Linux container or VM
+instead.
+
## Example site
[irregular.ninja](https://irregular.ninja) is a live photo album built with
@@ -33,6 +43,9 @@ shuriken --clean # remove ./dist and leftover staging dirs
```
ImageMagick (`magick` or `convert`) and Bash 5.1 or newer are required.
+GNU coreutils/findutils (the default `find`, `stat`, `cp`, and `sort` on a
+Linux distribution) are also required; the BSD/macOS equivalents are not
+sufficient (see *Platform compatibility* above).
## Main flags
diff --git a/bin/shuriken b/bin/shuriken
index e70b770..72adeb8 100755
--- a/bin/shuriken
+++ b/bin/shuriken
@@ -124,6 +124,69 @@ log_warning() {
printf 'WARNING: %s\n' "$*" >&2
}
+# 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).
+#
+# 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.
+
+require_gnu_tools() {
+ local probe_dir
+ local probe_out
+ local failed=''
+
+ 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
+
+ # GNU find supports the -printf action; BSD/macOS find does not.
+ 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" =~ ^[0-9]+$ ]] \
+ || failed='stat (missing the -c option)'
+ fi
+
+ # GNU cp supports the -a archive flag; minimal/older BSD cp variants lack it.
+ 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 \
+ || [ ! -f "$probe_dir/dest/inner" ]; then
+ failed='cp (missing the -a option)'
+ fi
+ fi
+
+ # 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) \
+ && [ -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
+
+ return 0
+}
+
# Inlined from src/lib/bootstrap.source.sh
# Startup wiring: the CLI usage/help text plus the shared config-array parser.
# Logging utilities now live in logging.source.sh and path resolution in
@@ -6438,6 +6501,12 @@ main() {
return "$status"
fi
+ require_gnu_tools
+ status=$?
+ if (( status != 0 )); then
+ return "$status"
+ fi
+
run_action
status=$?
if (( status != 0 )); then
diff --git a/src/lib/compat.source.sh b/src/lib/compat.source.sh
new file mode 100644
index 0000000..718c7ed
--- /dev/null
+++ b/src/lib/compat.source.sh
@@ -0,0 +1,61 @@
+# 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).
+#
+# 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.
+
+require_gnu_tools() {
+ local probe_dir
+ local probe_out
+ local failed=''
+
+ 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
+
+ # GNU find supports the -printf action; BSD/macOS find does not.
+ 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" =~ ^[0-9]+$ ]] \
+ || failed='stat (missing the -c option)'
+ fi
+
+ # GNU cp supports the -a archive flag; minimal/older BSD cp variants lack it.
+ 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 \
+ || [ ! -f "$probe_dir/dest/inner" ]; then
+ failed='cp (missing the -a option)'
+ fi
+ fi
+
+ # 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) \
+ && [ -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
+
+ return 0
+}
diff --git a/src/shuriken.sh b/src/shuriken.sh
index 16d846f..d9f7d1b 100755
--- a/src/shuriken.sh
+++ b/src/shuriken.sh
@@ -93,6 +93,8 @@ declare -r SHURIKEN_SOURCE_DIR
# SHURIKEN_LIB_SOURCES_BEGIN
# shellcheck source=src/lib/logging.source.sh
source "$SHURIKEN_SOURCE_DIR/lib/logging.source.sh"
+# shellcheck source=src/lib/compat.source.sh
+source "$SHURIKEN_SOURCE_DIR/lib/compat.source.sh"
# shellcheck source=src/lib/bootstrap.source.sh
source "$SHURIKEN_SOURCE_DIR/lib/bootstrap.source.sh"
# shellcheck source=src/lib/paths.source.sh
@@ -167,6 +169,12 @@ main() {
return "$status"
fi
+ require_gnu_tools
+ status=$?
+ if (( status != 0 )); then
+ return "$status"
+ fi
+
run_action
status=$?
if (( status != 0 )); then
diff --git a/tests/cli.sh b/tests/cli.sh
index e3af898..3ce8939 100755
--- a/tests/cli.sh
+++ b/tests/cli.sh
@@ -6190,6 +6190,79 @@ test_empty_args_fail() {
test::assert_contains 'Usage:' "$output"
}
+# The runtime GNU-tool guard (require_gnu_tools in src/lib/compat.source.sh)
+# must reject invocations when a core tool lacks the GNU-only feature shuriken
+# depends on. Each case installs a complete coreutils set via the shared
+# helper (so the environment is otherwise real), then replaces exactly one tool
+# with a fake that mimics the BSD/macOS behavior (rejecting the GNU flag), runs a
+# valid action, and asserts a clear GNU-only error. The guard runs before any
+# action work, so even --version is gated.
+test_gnu_tool_guard_rejects_non_gnu_find() {
+ local path_bin
+ local real_find
+ local output
+
+ test::setup
+ path_bin="$TEST_TMPDIR/bin"
+ real_find=$(command -v find)
+ test::install_coreutils_without_imagemagick "$path_bin"
+ rm -f "$path_bin/find"
+ cat > "$path_bin/find" <<FAKE
+#!/usr/bin/env bash
+set -euo pipefail
+for arg in "\$@"; do
+ if [ "\$arg" = -printf ]; then
+ printf 'find: unknown predicate -printf\n' >&2
+ exit 2
+ fi
+done
+exec "$real_find" "\$@"
+FAKE
+ chmod 0755 "$path_bin/find"
+
+ output=$(
+ cd "$TEST_TMPDIR"
+ PATH="$path_bin" test::capture_failure_output \
+ "$TEST_SHURIKEN" --version
+ )
+
+ test::assert_contains 'shuriken requires the GNU versions' "$output"
+ test::assert_contains 'find (missing the -printf action)' "$output"
+ test::teardown
+}
+
+test_gnu_tool_guard_rejects_non_gnu_stat() {
+ local path_bin
+ local real_stat
+ local output
+
+ test::setup
+ path_bin="$TEST_TMPDIR/bin"
+ real_stat=$(command -v stat)
+ test::install_coreutils_without_imagemagick "$path_bin"
+ rm -f "$path_bin/stat"
+ cat > "$path_bin/stat" <<FAKE
+#!/usr/bin/env bash
+set -euo pipefail
+if [ "\${1:-}" = -c ]; then
+ printf 'stat: illegal option -c\n' >&2
+ exit 1
+fi
+exec "$real_stat" "\$@"
+FAKE
+ chmod 0755 "$path_bin/stat"
+
+ output=$(
+ cd "$TEST_TMPDIR"
+ PATH="$path_bin" test::capture_failure_output \
+ "$TEST_SHURIKEN" --version
+ )
+
+ test::assert_contains 'shuriken requires the GNU versions' "$output"
+ test::assert_contains 'stat (missing the -c option)' "$output"
+ test::teardown
+}
+
test_extra_args_fail() {
test::assert_failure 'extra operand is rejected' "$TEST_SHURIKEN" --version extra
test::assert_failure \
@@ -7040,6 +7113,12 @@ main() {
test::run_case 'empty args fail' test_empty_args_fail
test::run_case 'extra args fail' test_extra_args_fail
test::run_case 'missing option values fail' test_missing_option_values_fail
+ test::run_case \
+ 'GNU-tool guard rejects non-GNU find' \
+ test_gnu_tool_guard_rejects_non_gnu_find
+ test::run_case \
+ 'GNU-tool guard rejects non-GNU stat' \
+ test_gnu_tool_guard_rejects_non_gnu_stat
}
main "$@"
diff --git a/tests/helpers.sh b/tests/helpers.sh
index d258c79..e377737 100755
--- a/tests/helpers.sh
+++ b/tests/helpers.sh
@@ -695,6 +695,7 @@ test::install_coreutils_without_imagemagick() {
local -a names=(
basename
bash
+ cp
date
dirname
find
@@ -704,6 +705,7 @@ test::install_coreutils_without_imagemagick() {
rm
sed
sort
+ stat
tac
tar
wc