Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,20 @@ jobs:
NOTES_FILE=$(mktemp)
awk '/^## \['"$BASE_VERSION"'\]/{found=1; next} found && /^## /{exit} found{print}' CHANGELOG.md > "$NOTES_FILE"

# Asimov is a launcher plus a library and data files, so the asset is a
# tarball of all three rather than a single script. It unpacks to
# asimov-<version>/{bin,lib,data} and installs with scripts/install.sh.
STAGE="asimov-${FULL_VERSION}"
mkdir -p "$STAGE"
cp -a bin lib data scripts LICENSE README.md CHANGELOG.md \
com.stevegrunwell.asimov.plist "$STAGE/"
TARBALL="${STAGE}.tar.gz"
tar -czf "$TARBALL" "$STAGE"

gh release create "$TAG" \
--title "Asimov $TAG" \
--notes-file "$NOTES_FILE" \
$PRERELEASE_FLAG \
asimov
"$TARBALL"

rm -f "$NOTES_FILE"
rm -rf "$NOTES_FILE" "$STAGE" "$TARBALL"
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,15 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Added

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

### Changed

- **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.
- 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.
- The remote installer (`scripts/install-remote.sh`) downloads a tarball instead of a single script, and installs under `~/.local`.
- Release assets are now `asimov-<version>.tar.gz` rather than a bare `asimov` script.

### Fixed

### Removed
Expand Down
49 changes: 40 additions & 9 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,20 +58,22 @@ The main script supports `--help`, `--version`, `--dry-run`, `--verbose`, and `-

## Adding a new dependency pattern

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

1. **Add the sentinel pair** to the `ASIMOV_VENDOR_DIR_SENTINELS` array in [`asimov`](asimov) — one `'directory sentinel'` entry per pattern.
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).
1. **Add a row** to [`data/sentinels.tsv`](data/sentinels.tsv) — one per pattern, fields separated by a **tab**.
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).
3. **Run `make check`** to verify your changes pass tests and linting.
4. **Add a changelog entry** under the `[Unreleased]` section in [`CHANGELOG.md`](CHANGELOG.md).

**Example sentinel entry:**
**Example sentinel row** (`dir`, `sentinel`, `ecosystem`, `note`):

```bash
'.zig-cache build.zig' # Zig build cache
```tsv
.zig-cache build.zig zig build cache
```

This means: exclude `.zig-cache/` only when `build.zig` exists in the same directory.
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`.

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

## Commit conventions

Expand Down Expand Up @@ -105,10 +107,28 @@ type(scope): short description
## Project structure

```
asimov # Main bash script
bin/asimov # Launcher: finds the library and data, then runs it
lib/asimov/
bootstrap.sh # Colours, constants, root and state-path resolution
config.sh # ~/.config/asimov/config
data.sh # Loaders for the data/ entities
cache.sh # ~/.cache/asimov state and the path cache
scan.sh # Which dirs to scan, and the find expression
discover.sh # Spotlight top-up on a cached run
exclude.sh # tmutil addexclusion, with its filters
report.sh # Usage, size formatting, run summary
prune.sh # The prune subcommand
doctor.sh # The doctor subcommand
main.sh # Argument parsing and dispatch
data/
sentinels.tsv # Directory + sentinel pairs
fixed-dirs.tsv # Global tool caches, always excluded
skip-paths.tsv # Directories never descended into
tests/
sentinels.bats # Tests for each dependency pattern
behavior.bats # Tests for edge cases and general behavior
cache.bats # Tests for the path cache
doctor.bats # Tests for the doctor subcommand
format.bats # Unit tests for format_size_kb()
plist.bats # Tests for the LaunchAgent plist
test_helper.bash # Shared setup/teardown and assertions
Expand All @@ -121,13 +141,24 @@ scripts/
Makefile # Build targets (test, lint, check, install, uninstall)
```

Installed, the three top-level pieces land under one prefix: `<prefix>/bin/asimov`,
`<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.

**Linting is whole-program.** `make lint` runs `shellcheck -x --source-path=. bin/asimov`,
which follows the `source` lines into every module. Running shellcheck on a module by
itself reports false "unused variable" and "referenced but not assigned" warnings,
because no single module is a complete program.

## Releasing (maintainers)

**The pipeline, end to end:**

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

Version = SemVer: new feature → **minor**, bug fix → **patch**. Pre-releases: `make release-beta` (GitHub pre-release; Homebrew ignores it). Details below.
Expand Down
13 changes: 7 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ help: ## Show this help
@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/'

version: ## Print asimov version
@./asimov --version
@./bin/asimov --version

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

lint: ## Run Shellcheck on all shell scripts
@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
@shellcheck -x --source-path=. bin/asimov
@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

check: test lint ## Run tests and linting

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

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


Expand All @@ -86,7 +87,7 @@ release: check ## Tag and push a stable release — GitHub Actions will create t
if [ -n "$$(git status --porcelain)" ]; then echo "error: working tree not clean"; exit 1; fi; \
BRANCH=$$(git rev-parse --abbrev-ref HEAD); \
if [ "$$BRANCH" != "main" ]; then echo "error: releases must be tagged from main (on $$BRANCH)"; exit 1; fi; \
VERSION=$$(./asimov --version); \
VERSION=$$(./bin/asimov --version); \
TAG="v$$VERSION"; \
if git rev-parse "$$TAG" >/dev/null 2>&1; then echo "error: $$TAG already exists"; exit 1; fi; \
echo "Tagging $$TAG (signed)..."; \
Expand All @@ -98,7 +99,7 @@ release: check ## Tag and push a stable release — GitHub Actions will create t
release-beta: check ## Tag and push a beta pre-release — GitHub Actions will create the pre-release
@set -e; \
if [ -n "$$(git status --porcelain)" ]; then echo "error: working tree not clean"; exit 1; fi; \
VERSION=$$(./asimov --version); \
VERSION=$$(./bin/asimov --version); \
BETA_NUM=1; \
while git tag | grep -q "^v$$VERSION-beta\.$$BETA_NUM$$"; do \
BETA_NUM=$$((BETA_NUM + 1)); \
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ brew services start asimov
curl -fsSL https://raw.githubusercontent.com/AsimovMac/asimov/main/scripts/install-remote.sh | bash
```

Installs `v0.10.0` to `~/.local/bin`. Found a bug? [Open an issue](https://github.com/AsimovMac/asimov/issues).
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).

### Quick start

Expand Down Expand Up @@ -286,10 +286,10 @@ cd asimov && make install
## Uninstall

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

## Upgrading
Expand Down
Loading
Loading