diff options
| -rw-r--r-- | Justfile | 2 | ||||
| -rw-r--r-- | README.md | 13 | ||||
| -rwxr-xr-x | bin/shuriken | 69 | ||||
| -rw-r--r-- | src/lib/compat.source.sh | 61 | ||||
| -rwxr-xr-x | src/shuriken.sh | 8 | ||||
| -rwxr-xr-x | tests/cli.sh | 79 | ||||
| -rwxr-xr-x | tests/helpers.sh | 2 |
7 files changed, 233 insertions, 1 deletions
@@ -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 @@ -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 |
