fix(scripts): make install.sh / build.sh work on stock macOS

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) <noreply@anthropic.com>
This commit is contained in:
2026-04-28 20:41:11 +03:00
parent 72022fc1dc
commit 074cbe9065
7 changed files with 128 additions and 2 deletions

View File

@@ -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.

View File

@@ -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: <semver>` in frontmatter and how `project-bootstrap` records them in a per-project manifest

View File

@@ -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)