From 94ef9dbf59a5638dbd839d34cf7f6f5034b11fa8 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sun, 19 Apr 2026 20:30:10 +0800 Subject: [PATCH 1/2] feat: add automatic nix garbage collection and fix macOS shell tests --- Makefile | 12 +++++++++-- home-manager/nix/nix.nix | 2 ++ nix-darwin/services/default.nix | 2 ++ nix-darwin/services/nix-gc/default.nix | 22 +++++++++++++++++++ spec/clipboard_copy_spec.sh | 25 ++++++++++----------- spec/clipboard_paste_spec.sh | 8 +++---- spec/notify_local_spec.sh | 10 ++++----- spec/secure_dotenv_spec.sh | 29 ++++++++++++++++++++----- spec/test_helpers.sh | 30 ++++++++++++++++++++++---- 9 files changed, 106 insertions(+), 34 deletions(-) create mode 100644 nix-darwin/services/nix-gc/default.nix diff --git a/Makefile b/Makefile index 9efcf58d3..e35ec3c6b 100644 --- a/Makefile +++ b/Makefile @@ -213,11 +213,19 @@ nvim-plugins-install: ## Download/build missing Neovim native plugin binaries (f switch: nix-switch services nvim-plugins-install dotagents-sync ## Apply Nix configuration, restart services, and sync plugins. .PHONY: clean -clean: ## Clean up old Nix generations and garbage collect. +clean: ## Clean up Nix generations older than 30 days and garbage collect. @echo "๐Ÿงน Cleaning up old generations and garbage collecting..." - @$(SUDO) nix-collect-garbage -d + @$(SUDO) nix-collect-garbage --delete-older-than 30d + @nix store optimise @echo "โœ… Cleanup complete" +.PHONY: clean-all +clean-all: ## Delete ALL old Nix generations and garbage collect (nuclear). + @echo "๐Ÿงน Removing all old generations..." + @$(SUDO) nix-collect-garbage -d + @nix store optimise + @echo "โœ… Full cleanup complete" + .PHONY: reset reset: git-submodule-sync ## Reset git status to clean (restore all changes and reinitialize submodules). @echo "๐Ÿ”„ Resetting git status to clean..." diff --git a/home-manager/nix/nix.nix b/home-manager/nix/nix.nix index fa2aacdea..fca582b57 100644 --- a/home-manager/nix/nix.nix +++ b/home-manager/nix/nix.nix @@ -29,6 +29,8 @@ }; gc = { automatic = true; + frequency = "weekly"; + options = "--delete-older-than 30d"; }; }; } diff --git a/nix-darwin/services/default.nix b/nix-darwin/services/default.nix index 37ff3ef7a..212b2c603 100644 --- a/nix-darwin/services/default.nix +++ b/nix-darwin/services/default.nix @@ -4,8 +4,10 @@ pkgs, }: let + nixGcModule = import ./nix-gc { inherit lib isRunner; }; pmsetBatteryPolicyModule = import ./pmset-battery-policy { inherit lib isRunner pkgs; }; in [ + nixGcModule pmsetBatteryPolicyModule ] diff --git a/nix-darwin/services/nix-gc/default.nix b/nix-darwin/services/nix-gc/default.nix new file mode 100644 index 000000000..3d8814675 --- /dev/null +++ b/nix-darwin/services/nix-gc/default.nix @@ -0,0 +1,22 @@ +{ + lib, + isRunner, + ... +}: +lib.mkIf (!isRunner) { + launchd.daemons."com.shunkakinoki.nix-gc" = { + script = '' + /nix/var/nix/profiles/default/bin/nix-collect-garbage --delete-older-than 30d + ''; + serviceConfig = { + RunAtLoad = false; + StartCalendarInterval = [ + { + Weekday = 0; + Hour = 3; + Minute = 0; + } + ]; + }; + }; +} diff --git a/spec/clipboard_copy_spec.sh b/spec/clipboard_copy_spec.sh index 27ad83c98..8dc829911 100644 --- a/spec/clipboard_copy_spec.sh +++ b/spec/clipboard_copy_spec.sh @@ -43,7 +43,7 @@ setup() { mock_bin_setup xclip # Restrict PATH so higher-priority backends (real xclip, pbcopy, etc.) are hidden local bash_dir - bash_dir="$(dirname "$(readlink -f "$(command -v bash)")")" + bash_dir="$(resolve_cmd_dir bash)" export PATH="$MOCK_BIN:$bash_dir" unset WAYLAND_DISPLAY } @@ -64,7 +64,7 @@ setup() { mock_bin_setup xsel # Restrict PATH so higher-priority backends (real xclip, pbcopy, etc.) are hidden local bash_dir - bash_dir="$(dirname "$(readlink -f "$(command -v bash)")")" + bash_dir="$(resolve_cmd_dir bash)" export PATH="$MOCK_BIN:$bash_dir" unset WAYLAND_DISPLAY } @@ -86,15 +86,13 @@ setup() { MOCK_ORIGINAL_PATH="${PATH:-}" MOCK_ORIGINAL_WAYLAND="${WAYLAND_DISPLAY:-}" MOCK_ORIGINAL_SSH_TTY="${SSH_TTY:-}" - local bash_dir - bash_dir="$(dirname "$(readlink -f "$(command -v bash)")")" - local base64_dir - base64_dir="$(dirname "$(readlink -f "$(command -v base64)")")" - local tr_dir - tr_dir="$(dirname "$(readlink -f "$(command -v tr)")")" - local printf_dir - printf_dir="$(dirname "$(readlink -f "$(command -v printf)")")" - export PATH="$MOCK_BIN:$bash_dir:$base64_dir:$tr_dir:$printf_dir" + # Symlink only the commands the script needs into MOCK_BIN to avoid + # leaking pbcopy/xclip/etc. from shared directories like /usr/bin + local cmd + for cmd in bash base64 tr printf; do + ln -sf "$(command -v "$cmd")" "$MOCK_BIN/$cmd" + done + export PATH="$MOCK_BIN" unset WAYLAND_DISPLAY export SSH_TTY=/dev/pts/0 export MOCK_BIN MOCK_ORIGINAL_PATH MOCK_ORIGINAL_WAYLAND MOCK_ORIGINAL_SSH_TTY @@ -128,9 +126,8 @@ setup() { MOCK_ORIGINAL_PATH="${PATH:-}" MOCK_ORIGINAL_WAYLAND="${WAYLAND_DISPLAY:-}" MOCK_ORIGINAL_SSH_TTY="${SSH_TTY:-}" - local bash_dir - bash_dir="$(dirname "$(readlink -f "$(command -v bash)")")" - export PATH="$MOCK_BIN:$bash_dir" + ln -sf "$(command -v bash)" "$MOCK_BIN/bash" + export PATH="$MOCK_BIN" unset WAYLAND_DISPLAY SSH_TTY TMUX export MOCK_BIN MOCK_ORIGINAL_PATH MOCK_ORIGINAL_WAYLAND MOCK_ORIGINAL_SSH_TTY } diff --git a/spec/clipboard_paste_spec.sh b/spec/clipboard_paste_spec.sh index 5b4d3f4dc..176393b50 100644 --- a/spec/clipboard_paste_spec.sh +++ b/spec/clipboard_paste_spec.sh @@ -43,7 +43,7 @@ printf 'hello' EOF chmod +x "$MOCK_BIN/wl-paste" local bash_dir - bash_dir="$(dirname "$(readlink -f "$(command -v bash)")")" + bash_dir="$(resolve_cmd_dir bash)" export PATH="$MOCK_BIN:$bash_dir" } cleanup() { @@ -73,7 +73,7 @@ printf 'hello' EOF chmod +x "$MOCK_BIN/xclip" local bash_dir - bash_dir="$(dirname "$(readlink -f "$(command -v bash)")")" + bash_dir="$(resolve_cmd_dir bash)" export PATH="$MOCK_BIN:$bash_dir" } cleanup() { @@ -103,7 +103,7 @@ printf 'hello' EOF chmod +x "$MOCK_BIN/xsel" local bash_dir - bash_dir="$(dirname "$(readlink -f "$(command -v bash)")")" + bash_dir="$(resolve_cmd_dir bash)" export PATH="$MOCK_BIN:$bash_dir" unset WAYLAND_DISPLAY export MOCK_BIN MOCK_ORIGINAL_PATH MOCK_ORIGINAL_WAYLAND @@ -132,7 +132,7 @@ setup() { MOCK_ORIGINAL_PATH="${PATH:-}" MOCK_ORIGINAL_WAYLAND="${WAYLAND_DISPLAY:-}" local bash_dir - bash_dir="$(dirname "$(readlink -f "$(command -v bash)")")" + bash_dir="$(resolve_cmd_dir bash)" export PATH="$MOCK_BIN:$bash_dir" unset WAYLAND_DISPLAY export MOCK_BIN MOCK_ORIGINAL_PATH MOCK_ORIGINAL_WAYLAND diff --git a/spec/notify_local_spec.sh b/spec/notify_local_spec.sh index 39c34f6a0..8429a6a2e 100644 --- a/spec/notify_local_spec.sh +++ b/spec/notify_local_spec.sh @@ -55,8 +55,8 @@ setup() { # Hide osascript so notify-send path is taken # Use readlink -f to resolve to nix store path (avoids NixOS profile bin with all packages) local bash_dir cat_dir - bash_dir="$(dirname "$(readlink -f "$(command -v bash)")")" - cat_dir="$(dirname "$(readlink -f "$(command -v cat)")")" + bash_dir="$(resolve_cmd_dir bash)" + cat_dir="$(resolve_cmd_dir cat)" export PATH="$MOCK_BIN:$bash_dir:$cat_dir" } cleanup() { @@ -80,8 +80,8 @@ setup() { # Hide osascript and notify-send so terminal-notifier path is taken # Use readlink -f to resolve to nix store path (avoids NixOS profile bin with all packages) local bash_dir cat_dir - bash_dir="$(dirname "$(readlink -f "$(command -v bash)")")" - cat_dir="$(dirname "$(readlink -f "$(command -v cat)")")" + bash_dir="$(resolve_cmd_dir bash)" + cat_dir="$(resolve_cmd_dir cat)" export PATH="$MOCK_BIN:$bash_dir:$cat_dir" } cleanup() { @@ -105,7 +105,7 @@ setup() { MOCK_ORIGINAL_PATH="${PATH:-}" # Use readlink -f to resolve to nix store path (avoids NixOS profile bin with all packages) local bash_dir - bash_dir="$(dirname "$(readlink -f "$(command -v bash)")")" + bash_dir="$(resolve_cmd_dir bash)" export PATH="$MOCK_BIN:$bash_dir" export MOCK_BIN MOCK_ORIGINAL_PATH } diff --git a/spec/secure_dotenv_spec.sh b/spec/secure_dotenv_spec.sh index 0fe8df6c9..3d8ff657d 100644 --- a/spec/secure_dotenv_spec.sh +++ b/spec/secure_dotenv_spec.sh @@ -61,15 +61,34 @@ setup() { # Create a symlink (should be skipped) ln -s "$TEST_HOME/.env" "$TEST_HOME/.env.link" + # Create a portable stat wrapper that accepts GNU stat -c '%a' syntax + STAT_WRAPPER="$TEST_HOME/portable-stat.sh" + cat >"$STAT_WRAPPER" <<'WRAPPER' +#!/usr/bin/env bash +if stat -c '%a' "$0" >/dev/null 2>&1; then + exec stat "$@" +fi +# macOS: translate -c '%a' to -f '%Lp' +args=() +while [[ $# -gt 0 ]]; do + case "$1" in + -c) shift; args+=(-f "$(echo "$1" | sed "s/%a/%Lp/g")"); shift ;; + *) args+=("$1"); shift ;; + esac +done +exec stat "${args[@]}" +WRAPPER + chmod +x "$STAT_WRAPPER" + # Preprocess the script, replacing placeholders with real commands PROCESSED_SCRIPT="$TEST_HOME/secure-dotenv-test.sh" sed \ -e "s|@find@|$(command -v find)|g" \ - -e "s|@stat@|$(command -v stat)|g" \ + -e "s|@stat@|$STAT_WRAPPER|g" \ "$SCRIPT" >"$PROCESSED_SCRIPT" chmod +x "$PROCESSED_SCRIPT" - export TEST_HOME PROCESSED_SCRIPT + export TEST_HOME PROCESSED_SCRIPT STAT_WRAPPER } cleanup() { rm -rf "$TEST_HOME" @@ -84,17 +103,17 @@ The status should be success End It 'changes .env.local from 755 to 600' -When run bash -c "bash '$PROCESSED_SCRIPT' '$TEST_HOME' && stat -c '%a' '$TEST_HOME/subdir/.env.local'" +When run bash -c "bash '$PROCESSED_SCRIPT' '$TEST_HOME' && '$STAT_WRAPPER' -c '%a' '$TEST_HOME/subdir/.env.local'" The output should equal '600' End It 'changes app.env from 644 to 600' -When run bash -c "bash '$PROCESSED_SCRIPT' '$TEST_HOME' && stat -c '%a' '$TEST_HOME/app.env'" +When run bash -c "bash '$PROCESSED_SCRIPT' '$TEST_HOME' && '$STAT_WRAPPER' -c '%a' '$TEST_HOME/app.env'" The output should equal '600' End It 'leaves already-600 files unchanged' -When run bash -c "bash '$PROCESSED_SCRIPT' '$TEST_HOME' && stat -c '%a' '$TEST_HOME/.env.safe'" +When run bash -c "bash '$PROCESSED_SCRIPT' '$TEST_HOME' && '$STAT_WRAPPER' -c '%a' '$TEST_HOME/.env.safe'" The output should equal '600' End diff --git a/spec/test_helpers.sh b/spec/test_helpers.sh index 73bb3ac81..eec9d8b0f 100644 --- a/spec/test_helpers.sh +++ b/spec/test_helpers.sh @@ -2,6 +2,26 @@ set -euo pipefail +# Portable alternative to `dirname "$(readlink -f "$(command -v cmd)")"`. +# macOS BSD readlink lacks -f; this uses Python as fallback. +resolve_cmd_dir() { + local cmd_path + cmd_path="$(command -v "$1")" + if readlink -f "$cmd_path" >/dev/null 2>&1; then + dirname "$(readlink -f "$cmd_path")" + else + dirname "$(python3 -c "import os; print(os.path.realpath('$cmd_path'))")" + fi +} + +# Portable file permission query (octal). macOS stat uses -f, GNU uses -c. +portable_stat_perms() { + if stat -c '%a' "$1" 2>/dev/null; then + return + fi + stat -f '%Lp' "$1" +} + mock_bin_setup() { MOCK_BIN="$(mktemp -d)" MOCK_LOG="$MOCK_BIN/mock.log" @@ -48,10 +68,12 @@ nix_script_preprocess() { # Replace @placeholder@ patterns with actual commands sed \ - -e 's|@aws@|aws|g' \ - -e 's|@rsync@|rsync|g' \ - -e 's|@bash@|bash|g' \ - -e 's|@sed@|sed|g' \ + -e "s|@aws@|aws|g" \ + -e "s|@rsync@|rsync|g" \ + -e "s|@bash@|bash|g" \ + -e "s|@sed@|sed|g" \ + -e "s|@find@|$(command -v find)|g" \ + -e "s|@stat@|$(command -v stat)|g" \ "$script" >"$processed" chmod +x "$processed" From e2a5405d546b62b93a5429ac6cf7524071cd2615 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sun, 19 Apr 2026 20:33:05 +0800 Subject: [PATCH 2/2] chore: format config/mempalace/default.nix --- config/mempalace/default.nix | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/config/mempalace/default.nix b/config/mempalace/default.nix index 20be5e2b2..c879166b6 100644 --- a/config/mempalace/default.nix +++ b/config/mempalace/default.nix @@ -1,10 +1,11 @@ -{config, ...}: let +{ config, ... }: +let homeDir = config.home.homeDirectory; - configText = builtins.replaceStrings - ["__HOME_DIR__"] - [homeDir] - (builtins.readFile ./config.json); -in { + configText = builtins.replaceStrings [ "__HOME_DIR__" ] [ homeDir ] ( + builtins.readFile ./config.json + ); +in +{ home.file.".mempalace/config.json" = { text = configText; force = true;