Skip to content

Commit 25677dc

Browse files
authored
refactor: split asimov into a launcher, a library, and data files (AsimovMac#126)
The single 1645-line script becomes bin/asimov (launcher), lib/asimov/*.sh (eleven modules, none over ~320 lines), and data/*.tsv (the directory lists). Installed, the library and data live at <prefix>/libexec/asimov and <prefix>/share/asimov. The launcher resolves its own physical path through symlinks, as Homebrew creates, and probes for both that layout and the repo layout, so ./bin/asimov works straight from a checkout. ASIMOV_LIB and ASIMOV_DATA override the probe. Sentinels, fixed directories and skip paths are now tab-separated records carrying their ecosystem or owning tool as a real field, loaded into parallel arrays (bash 3.2 has no associative arrays). sentinel_active_pairs() applies the config disabled/extra lists once and is the single source of active pairs for both find and Spotlight discovery, replacing the two copies of that filtering that had drifted apart. Behaviour is unchanged: all 219 existing tests pass untouched under bash 5 and bash 3.2. Adds tests/data.bats (13 tests) covering the record schemas and the loader, including a missing data file and a contributor-added sentinel. Sources are listed one per line rather than looped so that 'shellcheck -x --source-path=. bin/asimov' checks the whole program as a single unit; linting a module alone reports false unused-variable warnings. Co-authored-by: django23 <827397+django23@users.noreply.github.com>
1 parent d1e33ec commit 25677dc

29 files changed

Lines changed: 2244 additions & 1692 deletions

.github/workflows/release.yml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,20 @@ jobs:
3737
NOTES_FILE=$(mktemp)
3838
awk '/^## \['"$BASE_VERSION"'\]/{found=1; next} found && /^## /{exit} found{print}' CHANGELOG.md > "$NOTES_FILE"
3939
40+
# Asimov is a launcher plus a library and data files, so the asset is a
41+
# tarball of all three rather than a single script. It unpacks to
42+
# asimov-<version>/{bin,lib,data} and installs with scripts/install.sh.
43+
STAGE="asimov-${FULL_VERSION}"
44+
mkdir -p "$STAGE"
45+
cp -a bin lib data scripts LICENSE README.md CHANGELOG.md \
46+
com.stevegrunwell.asimov.plist "$STAGE/"
47+
TARBALL="${STAGE}.tar.gz"
48+
tar -czf "$TARBALL" "$STAGE"
49+
4050
gh release create "$TAG" \
4151
--title "Asimov $TAG" \
4252
--notes-file "$NOTES_FILE" \
4353
$PRERELEASE_FLAG \
44-
asimov
54+
"$TARBALL"
4555
46-
rm -f "$NOTES_FILE"
56+
rm -rf "$NOTES_FILE" "$STAGE" "$TARBALL"

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,15 @@ This project adheres to [Semantic Versioning](http://semver.org/).
88

99
### Added
1010

11+
- `asimov doctor` now checks the data files and reports how many sentinels and fixed directories are loaded, so an incomplete install is diagnosed rather than just failing.
12+
1113
### Changed
1214

15+
- **Asimov is no longer a single file.** The launcher lives at `bin/asimov`, the logic in `lib/asimov/*.sh`, and the directory lists in `data/*.tsv`. Installed, those become `<prefix>/bin/asimov`, `<prefix>/libexec/asimov/` and `<prefix>/share/asimov/`. Behaviour is unchanged; every existing test passes untouched.
16+
- Sentinels, fixed directories and skip paths are now tab-separated data files instead of bash arrays. Each record carries its ecosystem or owning tool as a real field, so adding a pattern is a one-line edit with no bash syntax involved.
17+
- The remote installer (`scripts/install-remote.sh`) downloads a tarball instead of a single script, and installs under `~/.local`.
18+
- Release assets are now `asimov-<version>.tar.gz` rather than a bare `asimov` script.
19+
1320
### Fixed
1421

1522
### Removed

CONTRIBUTING.md

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,20 +58,22 @@ The main script supports `--help`, `--version`, `--dry-run`, `--verbose`, and `-
5858

5959
## Adding a new dependency pattern
6060

61-
This is the most common type of contribution. To add a new ecosystem or dependency directory:
61+
This is the most common type of contribution, and it needs no bash: sentinels live in a data file.
6262

63-
1. **Add the sentinel pair** to the `ASIMOV_VENDOR_DIR_SENTINELS` array in [`asimov`](asimov) — one `'directory sentinel'` entry per pattern.
64-
2. **Add a test** in [`tests/sentinels.bats`](tests/sentinels.bats) using `create_project` to build the fixture. Keep this file in sync with `ASIMOV_VENDOR_DIR_SENTINELS` (one test per sentinel pair).
63+
1. **Add a row** to [`data/sentinels.tsv`](data/sentinels.tsv) — one per pattern, fields separated by a **tab**.
64+
2. **Add a test** in [`tests/sentinels.bats`](tests/sentinels.bats) using `create_project` to build the fixture. Keep this file in sync with `data/sentinels.tsv` (one test per row).
6565
3. **Run `make check`** to verify your changes pass tests and linting.
6666
4. **Add a changelog entry** under the `[Unreleased]` section in [`CHANGELOG.md`](CHANGELOG.md).
6767

68-
**Example sentinel entry:**
68+
**Example sentinel row** (`dir`, `sentinel`, `ecosystem`, `note`):
6969

70-
```bash
71-
'.zig-cache build.zig' # Zig build cache
70+
```tsv
71+
.zig-cache build.zig zig build cache
7272
```
7373

74-
This means: exclude `.zig-cache/` only when `build.zig` exists in the same directory.
74+
This means: exclude `.zig-cache/` only when `build.zig` exists in the same directory. Glob metacharacters are allowed in the sentinel, e.g. `DerivedData *.xcodeproj`.
75+
76+
The same applies to global caches ([`data/fixed-dirs.tsv`](data/fixed-dirs.tsv)) and skipped directories ([`data/skip-paths.tsv`](data/skip-paths.tsv)). Paths in those two are **relative to the home directory** — write `.npm/_cacache`, not `~/.npm/_cacache`.
7577

7678
## Commit conventions
7779

@@ -105,10 +107,28 @@ type(scope): short description
105107
## Project structure
106108

107109
```
108-
asimov # Main bash script
110+
bin/asimov # Launcher: finds the library and data, then runs it
111+
lib/asimov/
112+
bootstrap.sh # Colours, constants, root and state-path resolution
113+
config.sh # ~/.config/asimov/config
114+
data.sh # Loaders for the data/ entities
115+
cache.sh # ~/.cache/asimov state and the path cache
116+
scan.sh # Which dirs to scan, and the find expression
117+
discover.sh # Spotlight top-up on a cached run
118+
exclude.sh # tmutil addexclusion, with its filters
119+
report.sh # Usage, size formatting, run summary
120+
prune.sh # The prune subcommand
121+
doctor.sh # The doctor subcommand
122+
main.sh # Argument parsing and dispatch
123+
data/
124+
sentinels.tsv # Directory + sentinel pairs
125+
fixed-dirs.tsv # Global tool caches, always excluded
126+
skip-paths.tsv # Directories never descended into
109127
tests/
110128
sentinels.bats # Tests for each dependency pattern
111129
behavior.bats # Tests for edge cases and general behavior
130+
cache.bats # Tests for the path cache
131+
doctor.bats # Tests for the doctor subcommand
112132
format.bats # Unit tests for format_size_kb()
113133
plist.bats # Tests for the LaunchAgent plist
114134
test_helper.bash # Shared setup/teardown and assertions
@@ -121,13 +141,24 @@ scripts/
121141
Makefile # Build targets (test, lint, check, install, uninstall)
122142
```
123143

144+
Installed, the three top-level pieces land under one prefix: `<prefix>/bin/asimov`,
145+
`<prefix>/libexec/asimov/` and `<prefix>/share/asimov/`. The launcher resolves its
146+
own physical path (through symlinks, as Homebrew creates) and probes for both that
147+
layout and the repo layout, so `./bin/asimov` works straight from a checkout.
148+
`ASIMOV_LIB` and `ASIMOV_DATA` override the probe.
149+
150+
**Linting is whole-program.** `make lint` runs `shellcheck -x --source-path=. bin/asimov`,
151+
which follows the `source` lines into every module. Running shellcheck on a module by
152+
itself reports false "unused variable" and "referenced but not assigned" warnings,
153+
because no single module is a complete program.
154+
124155
## Releasing (maintainers)
125156

126157
**The pipeline, end to end:**
127158

128159
1. **Develop** on a branch → **PR into `main`**. CI (macOS 14 + 15) must pass; commits must be signed.
129160
2. **Merge** to `main`.
130-
3. **`make release`** tags `vX.Y.Z` (signed) → GitHub Actions publishes the release + `asimov` binary.
161+
3. **`make release`** tags `vX.Y.Z` (signed) → GitHub Actions publishes the release + the `asimov-X.Y.Z.tar.gz` asset.
131162
4. **Homebrew** autobumps `brew install asimov` on its own (~3h later). Nothing to do.
132163

133164
Version = SemVer: new feature → **minor**, bug fix → **patch**. Pre-releases: `make release-beta` (GitHub pre-release; Homebrew ignores it). Details below.

Makefile

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ help: ## Show this help
1414
@grep -E '(^[a-zA-Z0-9_-]+:.*?##.*$$)|(^##)' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}{printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}' | sed -e 's/\[32m##/[33m/'
1515

1616
version: ## Print asimov version
17-
@./asimov --version
17+
@./bin/asimov --version
1818

1919
exclusions: ## List all paths excluded from Time Machine
2020
@sudo mdfind "com_apple_backup_excludeItem = 'com.apple.backupd'"
@@ -38,7 +38,8 @@ test-system-bash: ## Run Bats tests under the macOS system bash (3.2), as shippe
3838
@$(MAKE) --no-print-directory test BASH_BIN=/bin/bash
3939

4040
lint: ## Run Shellcheck on all shell scripts
41-
@shellcheck asimov scripts/install.sh scripts/install-remote.sh scripts/uninstall.sh scripts/prep-release.sh scripts/test.sh tests/test_helper.bash tests/bin/run-tests.sh tests/bin/tmutil tests/bin/mdfind tests/bin/launchctl
41+
@shellcheck -x --source-path=. bin/asimov
42+
@shellcheck scripts/install.sh scripts/install-remote.sh scripts/uninstall.sh scripts/prep-release.sh scripts/test.sh tests/test_helper.bash tests/bin/run-tests.sh tests/bin/tmutil tests/bin/mdfind tests/bin/launchctl
4243

4344
check: test lint ## Run tests and linting
4445

@@ -49,7 +50,7 @@ bench:
4950
@bash -c 'time HOME="$(CURDIR)/tests/fixture" /tmp/asimov-v042 --dry-run'
5051
@echo ""
5152
@echo "=== v0.5.x (directory=tests/fixture) ==="
52-
@bash -c 'time HOME="$(CURDIR)/tests/fixture" ./asimov --dry-run "$(CURDIR)/tests/fixture"'
53+
@bash -c 'time HOME="$(CURDIR)/tests/fixture" ./bin/asimov --dry-run "$(CURDIR)/tests/fixture"'
5354
@rm -f /tmp/asimov-v042
5455

5556
## bench-home: Compare dry-run scan timing: current vs v0.4.2, against real home directory
@@ -59,7 +60,7 @@ bench-home:
5960
@bash -c 'time /tmp/asimov-v042 --dry-run'
6061
@echo ""
6162
@echo "=== v0.5.x (full home) ==="
62-
@bash -c 'time ./asimov --dry-run'
63+
@bash -c 'time ./bin/asimov --dry-run'
6364
@rm -f /tmp/asimov-v042
6465

6566

@@ -86,7 +87,7 @@ release: check ## Tag and push a stable release — GitHub Actions will create t
8687
if [ -n "$$(git status --porcelain)" ]; then echo "error: working tree not clean"; exit 1; fi; \
8788
BRANCH=$$(git rev-parse --abbrev-ref HEAD); \
8889
if [ "$$BRANCH" != "main" ]; then echo "error: releases must be tagged from main (on $$BRANCH)"; exit 1; fi; \
89-
VERSION=$$(./asimov --version); \
90+
VERSION=$$(./bin/asimov --version); \
9091
TAG="v$$VERSION"; \
9192
if git rev-parse "$$TAG" >/dev/null 2>&1; then echo "error: $$TAG already exists"; exit 1; fi; \
9293
echo "Tagging $$TAG (signed)..."; \
@@ -98,7 +99,7 @@ release: check ## Tag and push a stable release — GitHub Actions will create t
9899
release-beta: check ## Tag and push a beta pre-release — GitHub Actions will create the pre-release
99100
@set -e; \
100101
if [ -n "$$(git status --porcelain)" ]; then echo "error: working tree not clean"; exit 1; fi; \
101-
VERSION=$$(./asimov --version); \
102+
VERSION=$$(./bin/asimov --version); \
102103
BETA_NUM=1; \
103104
while git tag | grep -q "^v$$VERSION-beta\.$$BETA_NUM$$"; do \
104105
BETA_NUM=$$((BETA_NUM + 1)); \

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ brew services start asimov
3030
curl -fsSL https://raw.githubusercontent.com/AsimovMac/asimov/main/scripts/install-remote.sh | bash
3131
```
3232

33-
Installs `v0.10.0` to `~/.local/bin`. Found a bug? [Open an issue](https://github.com/AsimovMac/asimov/issues).
33+
Installs to `~/.local` (`bin/asimov`, plus its library and data files under `libexec/` and `share/`). Found a bug? [Open an issue](https://github.com/AsimovMac/asimov/issues).
3434

3535
### Quick start
3636

@@ -286,10 +286,10 @@ cd asimov && make install
286286
## Uninstall
287287

288288
```sh
289-
rm ~/.local/bin/asimov # curl install
290-
brew uninstall asimov # Homebrew
291-
launchctl bootout gui/$(id -u)/com.stevegrunwell.asimov # stop schedule
292-
make uninstall # source install
289+
rm -rf ~/.local/bin/asimov ~/.local/libexec/asimov ~/.local/share/asimov # curl install
290+
brew uninstall asimov # Homebrew
291+
launchctl bootout gui/$(id -u)/com.stevegrunwell.asimov # stop schedule
292+
make uninstall # source install
293293
```
294294

295295
## Upgrading

0 commit comments

Comments
 (0)