Skip to content

Commit 5d4263d

Browse files
authored
feat: exclude well-known global cache directories (#3)
* docs(readme): clarify that asimov only affects Time Machine, not Spotlight Add a note explaining that asimov does not hide directories from Spotlight indexing, with guidance on how to configure Spotlight privacy settings separately. Addresses stevegrunwell#90. * feat(sentinels): add .NET project build directory exclusions Exclude bin/ and obj/ directories when *.csproj (C#) or *.fsproj (F#) project files are present, using glob sentinel patterns. Inspired by stevegrunwell#87, props @guigomesa. * feat: skip already-excluded directories for faster subsequent runs Use Spotlight metadata (mdfind) to identify directories already excluded from Time Machine and skip them during the find traversal. Also fixes a comment typo and removes duplicate Gradle sentinel entries. Inspired by stevegrunwell#97, props @VladRassokhin. * feat: exclude well-known global cache directories Add fixed directory exclusions for common tool caches (~/.cache, ~/.gradle/caches, ~/.m2/repository, ~/.npm/_cacache, ~/.nuget/packages, ~/.kube/cache) that are always safe to exclude without sentinel files. Inspired by stevegrunwell#69, props @pkuczynski.
1 parent f8b6755 commit 5d4263d

File tree

6 files changed

+120
-5
lines changed

6 files changed

+120
-5
lines changed

CHANGELOG.md

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

1111
* Support glob patterns in sentinel definitions, enabling wildcards like `*.xcodeproj` ([stevegrunwell/asimov#64], props @mdab121)
1212
* Exclude Xcode DerivedData when `*.xcodeproj` is present ([stevegrunwell/asimov#64], props @mdab121)
13+
* Exclude well-known global cache directories (`~/.cache`, `~/.gradle/caches`, `~/.m2/repository`, `~/.npm/_cacache`, `~/.nuget/packages`, `~/.kube/cache`, etc.) without requiring sentinel files (inspired by [stevegrunwell/asimov#69], props @pkuczynski)
1314
* Exclude Next.js build cache (`.next`)
1415
* Exclude Nuxt build cache (`.nuxt`)
1516
* Exclude Angular CLI cache (`.angular`)
@@ -35,11 +36,17 @@ This project adheres to [Semantic Versioning](http://semver.org/).
3536

3637
### Changed
3738

39+
* Skip directories already excluded from Time Machine backups for faster subsequent runs (inspired by [stevegrunwell/asimov#97], props @VladRassokhin)
3840
* Migrated test suite from PHP/PHPUnit to [Bats](https://github.com/bats-core/bats-core) (Bash Automated Testing System), removing the PHP dependency for contributors
3941
* Replaced Travis CI pipeline with GitHub Actions (macOS 14 + 15 matrix)
4042
* Replaced PHP `tmutil` mock with a pure bash implementation
4143
* Moved install script to `scripts/install.sh` with shared variables, now copies binary instead of symlinking ([#35], props @sylver)
4244

45+
### Fixed
46+
47+
* Fixed duplicate Gradle sentinel entries in the sentinels list
48+
* Fixed typo in comment ("decendents" → "descendants")
49+
4350
### Removed
4451

4552
* Removed PHP test infrastructure (`composer.json`, `phpunit.xml.dist`, and PHP test files)
@@ -121,4 +128,6 @@ Initial public release.
121128
[#35]: https://github.com/stevegrunwell/asimov/pull/35
122129
[#56]: https://github.com/stevegrunwell/asimov/pull/56
123130
[stevegrunwell/asimov#64]: https://github.com/stevegrunwell/asimov/pull/64
131+
[stevegrunwell/asimov#69]: https://github.com/stevegrunwell/asimov/pull/69
124132
[stevegrunwell/asimov#87]: https://github.com/stevegrunwell/asimov/pull/87
133+
[stevegrunwell/asimov#97]: https://github.com/stevegrunwell/asimov/pull/97

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ test: ## Run Bats tests
99
@bats tests/sentinels.bats tests/behavior.bats
1010

1111
lint: ## Run Shellcheck on all shell scripts
12-
@shellcheck asimov scripts/install.sh scripts/uninstall.sh tests/bin/run-tests.sh tests/bin/tmutil
12+
@shellcheck asimov scripts/install.sh scripts/uninstall.sh tests/bin/run-tests.sh tests/bin/tmutil tests/bin/mdfind
1313

1414
check: test lint ## Run tests and linting
1515

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Asimov recognizes dependency directories across **30+ patterns** in these ecosys
4949
| **R** | `renv` |
5050
| **DevOps / IaC** | `.terraform`, `.terragrunt-cache`, `.vagrant`, `.direnv`, `cdk.out` |
5151
| **Game dev** | `.godot` |
52+
| **Global caches** | `~/.cache`, `~/.gradle/caches`, `~/.m2/repository`, `~/.npm/_cacache`, `~/.nuget/packages`, `~/.kube/cache` |
5253

5354
Each directory is only excluded when its corresponding config file (the "sentinel") exists — so `node_modules` is only excluded if `package.json` is present, `vendor` only if `composer.json`, `go.mod`, or `Gemfile` exists, etc.
5455

@@ -94,7 +95,9 @@ brew uninstall asimov
9495

9596
## How it works
9697

97-
Asimov is a thin wrapper around Apple's [`tmutil`](https://ss64.com/mac/tmutil.html). It builds a single `find` command from all known dependency patterns, walks your home directory (skipping `~/Library` and `~/.Trash`), and pipes matching paths through `tmutil addexclusion`. Directories already excluded are skipped automatically — safe to run as often as you like.
98+
Asimov is a thin wrapper around Apple's [`tmutil`](https://ss64.com/mac/tmutil.html). It builds a single `find` command from all known dependency patterns, walks your home directory (skipping `~/Library` and `~/.Trash`), and pipes matching paths through `tmutil addexclusion`. It also unconditionally excludes well-known global tool caches (like `~/.cache`, `~/.gradle/caches`, and `~/.npm/_cacache`) that can always be safely restored. Directories already excluded are skipped automatically — safe to run as often as you like.
99+
100+
> **Note:** Asimov only excludes directories from Time Machine backups. It does not affect Spotlight indexing. To prevent Spotlight from indexing a directory, add it to the Privacy tab in System Settings > Spotlight (or Siri & Spotlight on newer macOS versions).
98101
99102
> **Note:** Asimov only excludes directories from Time Machine backups. It does not affect Spotlight indexing. To prevent Spotlight from indexing a directory, add it to the Privacy tab in System Settings > Spotlight (or Siri & Spotlight on newer macOS versions).
100103

asimov

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ set -Eeu -o pipefail
2020
readonly ASIMOV_ROOT=~
2121

2222
# Paths to unconditionally skip over. This prevents Asimov from modifying the
23-
# Time Machine exclusions for these paths (and decendents). It has an important
23+
# Time Machine exclusions for these paths (and descendants). It has an important
2424
# side-effect of speeding up the search.
2525
readonly ASIMOV_SKIP_PATHS=(
2626
~/.Trash
@@ -51,8 +51,6 @@ readonly ASIMOV_VENDOR_DIR_SENTINELS=(
5151
'Pods Podfile' # CocoaPods
5252
'DerivedData *.xcodeproj' # Xcode DerivedData
5353
'bower_components bower.json' # Bower (JavaScript)
54-
'build build.gradle' # Gradle
55-
'build build.gradle.kts' # Gradle Kotlin Script
5654
'build pubspec.yaml' # Flutter (Dart)
5755
'build setup.py' # Python
5856
'dist setup.py' # PyPI Publishing (Python)
@@ -98,6 +96,22 @@ readonly ASIMOV_VENDOR_DIR_SENTINELS=(
9896
'renv renv.lock' # renv (R)
9997
)
10098

99+
# A list of fixed directories to exclude from Time Machine backups.
100+
#
101+
# Unlike sentinel-based pairs above, these directories are always excluded
102+
# when they exist — they represent global tool caches and artifacts that
103+
# can be safely restored.
104+
readonly ASIMOV_FIXED_DIRS=(
105+
~/.cache # XDG cache directory
106+
~/.gradle/caches # Gradle download cache
107+
~/.gradle/wrapper # Gradle wrapper distributions
108+
~/.m2/repository # Maven local repository
109+
~/.npm/_cacache # npm content-addressable cache
110+
~/.nuget/packages # NuGet global packages
111+
~/.kube/cache # Kubernetes API cache
112+
~/.kube/http-cache # Kubernetes HTTP cache
113+
)
114+
101115
# Exclude the given paths from Time Machine backups.
102116
# Reads the newline-separated list of paths from stdin.
103117
exclude_file() {
@@ -121,6 +135,13 @@ for d in "${ASIMOV_SKIP_PATHS[@]}"; do
121135
find_parameters_skip+=( -not \( -path "${d}" -prune \) )
122136
done
123137

138+
# Skip directories already excluded from Time Machine backups.
139+
# Uses Spotlight metadata which may not report all exclusions — any missed
140+
# directories are still handled by the tmutil isexcluded check in exclude_file().
141+
while IFS= read -r line; do
142+
[[ -n "$line" ]] && find_parameters_skip+=( -not \( -path "${line}" -prune \) )
143+
done < <(mdfind -onlyin "${ASIMOV_ROOT}" "com_apple_backup_excludeItem = 'com.apple.backupd'" 2>/dev/null | sort -u)
144+
124145
# Iterate over the directory/sentinel pairs to construct the `find` expression.
125146
declare -a find_parameters_vendor=()
126147
for i in "${ASIMOV_VENDOR_DIR_SENTINELS[@]}"; do
@@ -159,3 +180,11 @@ printf '\n\033[0;36mFinding dependency directories with corresponding definition
159180
find "${ASIMOV_ROOT}" \( "${find_parameters_skip[@]}" \) \( -false "${find_parameters_vendor[@]}" \) \
160181
| exclude_file \
161182
;
183+
184+
# Exclude fixed directories (global tool caches) when they exist.
185+
printf '\n\033[0;36mExcluding known cache directories…\033[0m\n'
186+
for dir in "${ASIMOV_FIXED_DIRS[@]}"; do
187+
if [[ -d "$dir" ]]; then
188+
echo "$dir"
189+
fi
190+
done | exclude_file

tests/behavior.bats

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,69 @@ load test_helper
141141
refute_excluded "${HOME}/Code/My-Project/node_modules/dep/node_modules"
142142
[[ "$(count_exclusions)" -eq 1 ]]
143143
}
144+
145+
# =============================================================================
146+
# Skip already-excluded directories (mdfind optimization)
147+
# =============================================================================
148+
149+
@test "skips directories already excluded from Time Machine" {
150+
create_project "Code/Already-Excluded" "package.json" "node_modules"
151+
create_project "Code/New-Project" "package.json" "node_modules"
152+
153+
# Pre-exclude the first project manually
154+
echo "${HOME}/Code/Already-Excluded/node_modules" > "$ASIMOV_TEST_EXCLUSIONS"
155+
156+
# Tell mock mdfind to report it as already excluded
157+
ASIMOV_TEST_MDFIND_RESULTS="${TEST_TEMP_DIR}/.mdfind_results"
158+
export ASIMOV_TEST_MDFIND_RESULTS
159+
echo "${HOME}/Code/Already-Excluded" > "$ASIMOV_TEST_MDFIND_RESULTS"
160+
161+
run_asimov
162+
163+
# The new project should be excluded
164+
assert_excluded "${HOME}/Code/New-Project/node_modules"
165+
# The already-excluded one should still only have 1 entry (not duplicated)
166+
[[ "$(count_exclusions)" -eq 2 ]]
167+
}
168+
169+
# =============================================================================
170+
# Fixed directories (global caches)
171+
# =============================================================================
172+
173+
@test "excludes fixed directory when it exists" {
174+
mkdir -p "${HOME}/.cache"
175+
run_asimov
176+
assert_excluded "${HOME}/.cache"
177+
}
178+
179+
@test "does not fail when fixed directory does not exist" {
180+
# Don't create any fixed dirs — asimov should still succeed
181+
run_asimov
182+
[[ "$status" -eq 0 ]]
183+
[[ "$(count_exclusions)" -eq 0 ]]
184+
}
185+
186+
@test "excludes multiple fixed directories when they exist" {
187+
mkdir -p "${HOME}/.cache"
188+
mkdir -p "${HOME}/.gradle/caches"
189+
mkdir -p "${HOME}/.npm/_cacache"
190+
run_asimov
191+
assert_excluded "${HOME}/.cache"
192+
assert_excluded "${HOME}/.gradle/caches"
193+
assert_excluded "${HOME}/.npm/_cacache"
194+
[[ "$(count_exclusions)" -eq 3 ]]
195+
}
196+
197+
@test "does not re-exclude already excluded fixed directory" {
198+
mkdir -p "${HOME}/.cache"
199+
run_asimov
200+
assert_excluded "${HOME}/.cache"
201+
local first_count
202+
first_count="$(count_exclusions)"
203+
[[ "$first_count" -eq 1 ]]
204+
205+
run_asimov
206+
local second_count
207+
second_count="$(count_exclusions)"
208+
[[ "$second_count" -eq 1 ]]
209+
}

tests/bin/mdfind

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env bash
2+
# Mock mdfind for testing — returns nothing by default.
3+
# Set ASIMOV_TEST_MDFIND_RESULTS to a file path to return its contents.
4+
set -euo pipefail
5+
6+
if [[ -n "${ASIMOV_TEST_MDFIND_RESULTS:-}" && -f "${ASIMOV_TEST_MDFIND_RESULTS}" ]]; then
7+
cat "${ASIMOV_TEST_MDFIND_RESULTS}"
8+
fi

0 commit comments

Comments
 (0)