From 074cbe9065ef467ace769463433d979544f581cb Mon Sep 17 00:00:00 2001 From: vitya Date: Tue, 28 Apr 2026 20:41:11 +0300 Subject: [PATCH] fix(scripts): make install.sh / build.sh work on stock macOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stock macOS ships bash 3.2 (frozen 2007 due to GPLv3) and BSD find. Both scripts used `mapfile` (bash 4+) and `find -printf` (GNU only), so a fresh Mac user running `bash scripts/install.sh` died on line 11 before copying anything. Replace with a portable shell glob — same sort order, no `find` dependency, works on bash 3.2 and 4+. Verified on git-bash: 16 skills discovered, install dry-run copies all, `build.sh` produces a valid archive. See .wiki/concepts/install-portability.md for the full gotcha. Co-Authored-By: Claude Opus 4.7 (1M context) --- .tasks/STATUS.md | 8 ++++ .tasks/mac-support-scripts.md | 31 ++++++++++++ .wiki/concepts/install-portability.md | 69 +++++++++++++++++++++++++++ .wiki/index.md | 1 + .wiki/log.md | 1 + scripts/build.sh | 10 +++- scripts/install.sh | 10 +++- 7 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 .tasks/mac-support-scripts.md create mode 100644 .wiki/concepts/install-portability.md diff --git a/.tasks/STATUS.md b/.tasks/STATUS.md index d022cc8..3c51882 100644 --- a/.tasks/STATUS.md +++ b/.tasks/STATUS.md @@ -14,6 +14,14 @@ _Updated: 2026-04-28_ 🔵 Blocked — waiting on external input --> +## 🟢 [mac-support-scripts] — fix install.sh / build.sh for stock macOS +**Status:** done +**Where I stopped:** patched both scripts to drop `mapfile` (bash 4+) and `find -printf` (GNU find) — replaced with portable shell glob + `basename` + `sort`; verified on git-bash (16 skills discovered, install dry-run + build smoke-test passed); wiki concept page `install-portability.md` added; index + log updated +**Next action:** (none — kept until merged) +**Branch:** master + +--- + ## 🟡 [skill-readmes] — write English README.md for every skill + translate root README **Status:** paused **Where I stopped:** infra-skill cluster done — READMEs for `project-bootstrap`, `setup-wiki`, `setup-tasks`, `using-wiki`, `using-tasks`; root README translated; cross-links between all five skills wired up diff --git a/.tasks/mac-support-scripts.md b/.tasks/mac-support-scripts.md new file mode 100644 index 0000000..bb103b9 --- /dev/null +++ b/.tasks/mac-support-scripts.md @@ -0,0 +1,31 @@ +# mac-support-scripts + +## Goal +Make `scripts/install.sh` and `scripts/build.sh` work on stock macOS (BSD find, bash 3.2). Currently both use `mapfile` (bash 4+) and `find -printf` (GNU-only) — fatal on a fresh Mac. Without the fix, the README's "Linux / macOS (bash)" quick-start is a lie. + +## Key files +- `scripts/install.sh:11` — `mapfile -t names < <(find ... -printf ...)` +- `scripts/build.sh:18` — same pattern +- `README.md:30-38` — claims macOS bash quick-start works +- `.wiki/concepts/build-notes.md` — companion concept page; add a sibling for the install-script portability gotcha + +## Decisions log +- 2026-04-28: Replace `mapfile` + `find -printf` with shell glob (`for d in "$SRC"/*/`). Reason: works in bash 3.2 (stock macOS) and avoids a `find` flavor dependency. Alternative — require `bash 4+` via `#!/usr/bin/env bash` + version check — rejected: pushes the burden onto Mac users, who would need `brew install bash` for a 30-line script. +- 2026-04-28: Don't drop the `set -euo pipefail` line — POSIX-ish bash supports it from 3.x. +- 2026-04-28: Final — patched both scripts; verified on git-bash (`bash -n` clean, install dry-run installs all 16 skills sorted alphabetically into temp dir, `build.sh active-platform` produces a valid `.skill` archive); wrote `.wiki/concepts/install-portability.md`; index + log updated; dist/ unchanged (smoke-rebuild was byte-identical so no dist churn). + +## Open questions +- [ ] None — done. + +## Completed steps +- [x] Identify bugs +- [x] Pick fix approach (shell glob, no new deps) +- [x] Patch `scripts/install.sh` +- [x] Patch `scripts/build.sh` +- [x] Verify on git-bash (`bash -n` + install dry-run + build smoke-test) +- [x] Wiki concept page (`install-portability.md`) + index + log +- [x] Commit + +## Notes +- `basename` is in POSIX, fine on Mac. +- Empty `skills/` would have `"$SRC"/*/` literal under bash 3.2 without `nullglob`. Guard with `[ -d "$d" ] || continue` — works without `shopt`. diff --git a/.wiki/concepts/install-portability.md b/.wiki/concepts/install-portability.md new file mode 100644 index 0000000..c3ef039 --- /dev/null +++ b/.wiki/concepts/install-portability.md @@ -0,0 +1,69 @@ +--- +title: Install / Build Script Portability +type: concept +updated: 2026-04-28 +--- + +# Install / Build Script Portability + +`scripts/install.sh` and `scripts/build.sh` must run on three combos: + +1. Windows + git-bash (primary dev workstation) +2. Linux + bash (typical server) +3. macOS + bash (the gotcha) + +Stock macOS ships **bash 3.2** (frozen at 2007 because GPLv3) and **BSD `find`**. Both diverge from what Linux's bash 4+ / GNU `find` accept. + +## What broke before the fix + +Both scripts originally used: + +```bash +mapfile -t names < <(find "$SRC" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort) +``` + +Two failures on stock Mac: + +- **`mapfile`** — bash 4+ builtin. Bash 3.2 throws `mapfile: command not found`. +- **`find -printf`** — GNU extension. BSD find throws `find: -printf: unknown primary or operator` and exits non-zero, killing the pipeline under `set -euo pipefail`. + +Result: `bash scripts/install.sh` died on line 11 with a confusing error, before copying anything. Same for `build.sh`. + +## Fix + +Replace with shell glob — pure POSIX-compatible bash 3.2 syntax, no `find` flavor dependency: + +```bash +names=() +for d in "$SRC"/*/; do + [ -d "$d" ] || continue + names+=("$(basename "$d")") +done +IFS=$'\n' names=($(printf '%s\n' "${names[@]}" | sort)) +unset IFS +``` + +- `for d in dir/*/` — glob expansion. Trailing slash filters to directories. +- `[ -d "$d" ] || continue` — guards the empty-dir case (when no subdirs exist, the literal pattern `dir/*/` is left unexpanded by default; the test rejects it). +- `basename` — POSIX, present everywhere. +- `IFS=$'\n' ... sort` — preserves alphabetical order to match the previous behavior (`find | sort`). + +`$'...'` ANSI-C quoting is bash 2+. The whole approach works identically on bash 3.2 (Mac), bash 4 (Linux), bash 5 (modern Linux + brew on Mac), and the bash bundled with git-for-Windows. + +## What still requires bash, not POSIX `sh` + +The shebangs are still `#!/usr/bin/env bash`. Arrays (`names=()`, `names+=(...)`, `"${names[@]}"`) are bash, not POSIX. That's fine — bash is on every target OS. Demoting to `sh` would force a much uglier rewrite for no real portability gain. + +## What was already portable + +- `cp -R "$src" "$dst"` — POSIX flag, works everywhere. +- `rm -rf` — POSIX. +- `mkdir -p` — POSIX. +- `~/.claude/skills/` — `~` expands the same on all three shells. +- The `zip`/PowerShell branching in `build.sh` already handled "no zip on stock Windows" — the Mac path simply uses `zip`, which ships in `/usr/bin/zip` on every macOS release since forever. + +## Test coverage + +Manually verified on Windows + git-bash after the fix: 16 skills discovered, sorted, installed to `$CLAUDE_SKILLS_DIR`. `build.sh active-platform` produced a valid archive. No real BSD-find / bash-3.2 environment was tested — confidence comes from the fact that the new code uses only POSIX-shell + bash-2 features, all explicitly supported on stock macOS. + +The `archive-roundtrip-test` task on the board would catch any regression here automatically. diff --git a/.wiki/index.md b/.wiki/index.md index 9260799..0c7a314 100644 --- a/.wiki/index.md +++ b/.wiki/index.md @@ -14,6 +14,7 @@ Catalog of all wiki pages. One line per page, organized by type. Updated on ever - [active-platform-decision.md](concepts/active-platform-decision.md) — why `active-platform` is a skill (not a memory entry); why default = Windows; how it's wired into `project-bootstrap` - [build-notes.md](concepts/build-notes.md) — why `build.ps1` exists alongside `build.sh`; PS 5.1 backslash-in-zip gotcha; how to extract a `.skill` +- [install-portability.md](concepts/install-portability.md) — `install.sh` / `build.sh` rewritten to drop `mapfile` (bash 4+) and `find -printf` (GNU only) so stock macOS (bash 3.2 + BSD find) works - [context7-setup.md](concepts/context7-setup.md) — switched context7 from manual MCP entries to the official plugin; API key in `.mcp.json` as `--api-key`; now also captured as `setup-context7` skill (one-time install/migrate flow with key discovery) - [repo-layout.md](concepts/repo-layout.md) — flat `skills/`, committed `dist/`, bash + PowerShell scripts; install model - [skill-versioning.md](concepts/skill-versioning.md) — why infra skills carry `version: ` in frontmatter and how `project-bootstrap` records them in a per-project manifest diff --git a/.wiki/log.md b/.wiki/log.md index 7849d83..32fa6a9 100644 --- a/.wiki/log.md +++ b/.wiki/log.md @@ -27,3 +27,4 @@ Parseable: `grep "^## \[" .wiki/log.md | tail -20`. ## [2026-04-28] refactor | tasks split — `task-status-wiki` renamed to `using-tasks` (policy); new `setup-tasks` skill owns greenfield + interactive migration (no auto-parsing of old flat STATUS.md); `project-bootstrap` Step 4 delegates ## [2026-04-28] refactor | this repo's `.tasks/` migrated to canonical layout — flat `## Done`/`## Backlog` replaced by emoji-status board (7 ⚪ Ready blocks); historical Done entries dropped (preserved in git log); `.bak` ignored via .gitignore ## [2026-04-28] cleanup | removed stale `~/.claude/skills/{wiki-maintainer,task-status-wiki}/` installs (replaced by `using-wiki`/`using-tasks`); 16 skills installed, no duplicates; context7 plugin (mcp__plugin_context7_context7__*) confirmed live after restart +## [2026-04-28] decision | install-portability — `install.sh`/`build.sh` patched to drop `mapfile`+`find -printf`; stock macOS (bash 3.2 + BSD find) now works; verified on git-bash (16 skills discovered, sorted, installed; build.sh produces archive) diff --git a/scripts/build.sh b/scripts/build.sh index 044cf44..4c46e18 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -15,7 +15,15 @@ DIST="$ROOT/dist" if command -v zip >/dev/null 2>&1; then mkdir -p "$DIST" if [ "$#" -eq 0 ]; then - mapfile -t names < <(find "$SRC" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort) + # Portable across bash 3.2 (stock macOS) and bash 4+ (Linux, git-bash): + # avoid `mapfile` (bash 4+) and `find -printf` (GNU find only). + names=() + for d in "$SRC"/*/; do + [ -d "$d" ] || continue + names+=("$(basename "$d")") + done + IFS=$'\n' names=($(printf '%s\n' "${names[@]}" | sort)) + unset IFS else names=("$@") fi diff --git a/scripts/install.sh b/scripts/install.sh index 5f0af82..4117b87 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -8,7 +8,15 @@ SRC="$ROOT/skills" TARGET="${CLAUDE_SKILLS_DIR:-$HOME/.claude/skills}" if [ "$#" -eq 0 ]; then - mapfile -t names < <(find "$SRC" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort) + # Portable across bash 3.2 (stock macOS) and bash 4+ (Linux, git-bash): + # avoid `mapfile` (bash 4+) and `find -printf` (GNU find only). + names=() + for d in "$SRC"/*/; do + [ -d "$d" ] || continue + names+=("$(basename "$d")") + done + IFS=$'\n' names=($(printf '%s\n' "${names[@]}" | sort)) + unset IFS else names=("$@") fi