From b6af18017be909a8a6696c31dbd6857664592f5e Mon Sep 17 00:00:00 2001 From: David Tomaschik Date: Wed, 25 Mar 2026 08:25:03 -0700 Subject: [PATCH] Updates --- .Brewfile.ignore | 6 + .githooks/pre-commit | 1 + .githooks/pre-commit.d/05-check-brewfile | 20 +++ AGENTS.md | 22 +++- Brewfile | 15 ++- README.md | 40 ++++-- bin/macos/update_brewfile | 156 +++++++++++++++++++++++ install.sh | 7 + 8 files changed, 246 insertions(+), 21 deletions(-) create mode 100644 .Brewfile.ignore create mode 120000 .githooks/pre-commit create mode 100755 .githooks/pre-commit.d/05-check-brewfile create mode 100755 bin/macos/update_brewfile diff --git a/.Brewfile.ignore b/.Brewfile.ignore new file mode 100644 index 0000000..cbede01 --- /dev/null +++ b/.Brewfile.ignore @@ -0,0 +1,6 @@ +# Add package names here to ignore them in Brewfile updates. +# One package per line. +# Example: +# iterm2 +# wget +orbstack diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 120000 index 0000000..7bbabda --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1 @@ +githooks.sh \ No newline at end of file diff --git a/.githooks/pre-commit.d/05-check-brewfile b/.githooks/pre-commit.d/05-check-brewfile new file mode 100755 index 0000000..2e51b81 --- /dev/null +++ b/.githooks/pre-commit.d/05-check-brewfile @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# Check if Brewfile needs updating +if [[ "$(uname)" != "Darwin" ]]; then + exit 0 +fi + +# We use the script we just created +UPDATE_SCRIPT="bin/macos/update_brewfile" + +if [[ -x "$UPDATE_SCRIPT" ]]; then + # Run in dry-run mode and see if there's output + DIFF_OUTPUT=$("$UPDATE_SCRIPT" --dry-run 2>/dev/null) + if [[ "$DIFF_OUTPUT" == *"Changes detected"* ]]; then + echo "⚠️ Brewfile is out of sync with your installed packages." + echo " Run '$UPDATE_SCRIPT' to synchronize it." + echo "" + # We don't fail the commit, just warn. + fi +fi diff --git a/AGENTS.md b/AGENTS.md index 753a6e3..538344c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,10 +17,16 @@ may also be used at times. ## Project Structure -* `bin/`: Contains executable scripts that should be available in the shell's `PATH`. -* `dotfiles/`: Contains configuration files (dotfiles) to be symlinked into the home directory. -* `packages/`: Contains lists of packages to be installed by the `install.sh` script. Each file in this directory corresponds to a package set. -* `install.sh`: The main installation script that sets up the environment, symlinks dotfiles, and installs packages. +* `bin/`: Contains executable scripts symlinked to `~/bin/`. Subdirectories like `macos/`, `restic/`, and `setup/` are included. +* `dotfiles/`: Contains configuration files (dotfiles) symlinked to the home directory. +* `dotfile_overlays/`: Each directory within is symlinked to the home directory, allowing for modular or git-submodule-based configurations. +* `local_dotfiles/`: If present, its contents are symlinked to the home directory (ignored by git). +* `packages/`: Contains lists of packages (one per line) for different environments or toolsets. +* `keys/`: Contains SSH keys (`ssh/`), GPG keys (`gpg/`), and a `known_hosts` file to be installed/merged. +* `skeltools/`: Internal utilities used by the installation scripts. +* `sysctl/` and `udev/`: Linux system configuration files. +* `Brewfile`: Homebrew package list for macOS environments. +* `install.sh`: The primary installation script for symlinking and basic setup. ## Notes on Security Issues @@ -43,18 +49,20 @@ If making changes that affects how the user installs the tools, update ### Adding a new dotfile 1. Place the new dotfile in the `dotfiles/` directory. -2. The `install.sh` script will automatically symlink it to the home directory. +2. Alternatively, use `dotfile_overlays/` if the dotfile belongs to a specific group or submodule. +3. The `install.sh` script will automatically symlink it to the home directory. ### Adding a new script to `bin/` -1. Add the new script to the `bin/` directory. +1. Add the new script to the `bin/` directory (or an appropriate subdirectory). 2. Ensure the script is executable (`chmod +x`). ### Adding a new package 1. Identify the appropriate package list in the `packages/` directory (e.g., `packages/cli`, `packages/kali`). -2. Add the new package name to the list. +2. Add the new package name to the list (one per line). 3. If a new package set is required, create a new file in the `packages/` directory. +4. For macOS-specific packages, also consider adding them to the `Brewfile`. ### Platform-specific changes diff --git a/Brewfile b/Brewfile index 1a0b116..0e953f1 100644 --- a/Brewfile +++ b/Brewfile @@ -66,9 +66,7 @@ brew "zlib" brew "zsh-syntax-highlighting" brew "sass/sass/migrator" brew "sass/sass/sass" -cask "claude-code" cask "codeql" -cask "cryptomator" cask "cyberduck" cask "font-fira-code-nerd-font" cask "font-fira-mono-nerd-font" @@ -77,7 +75,6 @@ cask "font-hack-nerd-font" cask "font-inconsolata-nerd-font" cask "font-symbols-only-nerd-font" cask "font-terminess-ttf-nerd-font" -cask "gcloud-cli" cask "ghidra" cask "gimp" cask "github" @@ -85,7 +82,6 @@ cask "iterm2" cask "macfuse" cask "meld" cask "mitmproxy" -cask "orbstack" cask "raycast" cask "rectangle" cask "scroll-reverser" @@ -93,9 +89,18 @@ cask "temurin" cask "veracrypt" cask "zulu@17" +def is_corp? + # Check for MDM enrollment (Enrolled via DEP: Yes) + `profiles status -type enrollment 2>/dev/null`.include?("Enrolled via DEP: Yes") +end + # non-corp -if ENV['USER'] != "davidtomaschik" +if !is_corp? brew "bazel" brew "openssh" + cask "claude-code" + cask "cryptomator" + cask "gcloud-cli" cask "google-cloud-sdk" + cask "orbstack" end diff --git a/README.md b/README.md index c73cb51..5e92d77 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,24 @@ ### About ### This is a repository of configuration files that I like to have on all the -machines that I use. I can just clone the repository and run "repo/setup.sh" -and get most things setup the way I like them. +machines that I use. For new systems, you can bootstrap by running the +included `clone.sh` script: + +```bash +curl -L https://raw.githubusercontent.com/Matir/skel/master/clone.sh | bash +``` + +Alternatively, you can manually clone the repository and run `./install.sh`. This started just as dotfiles, but expanded to include SSH keys, GPG keys, -packages I like installed, and an ever-growing setup script. There are various +and an ever-growing setup script. There are various options to install just parts of it, such as on a machine where I only have a user account but no root. -This now uses [git-crypt](https://github.com/AGWA/git-crypt) to protect -`private_dotfiles` for things I don't want to splash all over the internet. :) +This environment supports using `dotfile_overlays/` or `local_dotfiles/` to +manage machine-specific or private configurations. You can use +[git-crypt](https://github.com/AGWA/git-crypt) on these overlay directories +for things you don't want to splash all over the internet. :) I still wouldn't check in anything terribly sensitive, like private keys. ### Usefulness ### @@ -44,16 +52,30 @@ sudo apt-get install xbindkeys xdotool After installation, the functionality will be enabled automatically on your next login. +On macOS, you can install the recommended packages using the included `Brewfile`: + +```bash +brew bundle install +``` + +### Packages ### + +The `packages/` directory contains lists of recommended packages. You can +manually install a set (e.g., on a Debian-based system) using: + +```bash +grep -v "^#" packages/cli | xargs sudo apt-get install -y +``` + ``` BASEDIR: Where the skel framework is installed. Defaults to $HOME/.skel MINIMAL: Don't do things that require git clones or installation of anything - not included in my .skel. (Defaults to 0, installs everything.) + not included in my .skel. (e.g., skips vim-plug, TPM) (Defaults to 0) INSTALL_KEYS: Install GnuPG and SSH keys. SSH keys are placed in authorized_keys. (Defaults to 1, installs keys.) TRUST_ALL_KEYS: Allow all keys to be used for SSH login, versus a small subset. -INSTALL_PKGS: Install common packages, if on a Debian-like system. - (Defaults to opposite of $MINIMAL.) -SAVE: Save the install options to ${BASEDIR}/installed-prefs +VERBOSE: Enable verbose output during installation. (Defaults to 0) +SAVE: Save the install options to ${BASEDIR}/.installed-prefs ``` ### TODO ### diff --git a/bin/macos/update_brewfile b/bin/macos/update_brewfile new file mode 100755 index 0000000..9a6af7e --- /dev/null +++ b/bin/macos/update_brewfile @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 + +import os +import subprocess +import re +import sys +import argparse +import difflib + +# Regex to match brew/cask/tap/mas lines +PKG_RE = re.compile(r'^\s*(brew|cask|tap|mas)\s+["\']([^"\']+)["\'](.*)$') + +def get_repo_root(): + try: + root = subprocess.check_output(['git', 'rev-parse', '--show-toplevel'], + stderr=subprocess.STDOUT).decode().strip() + return root + except subprocess.CalledProcessError: + return os.getcwd() + +def get_ignore_list(repo_root): + ignore = set() + paths = [ + os.path.join(repo_root, '.Brewfile.ignore'), + os.path.expanduser('~/.Brewfile.ignore'), + os.path.expanduser('~/.config/homebrew/ignore') + ] + for path in paths: + if os.path.exists(path): + with open(path) as f: + for line in f: + line = line.split('#')[0].strip() + if line: + ignore.add(line) + return ignore + +def get_current_packages(): + """Runs brew bundle dump and returns lines.""" + try: + output = subprocess.check_output(['brew', 'bundle', 'dump', '--file=-'], + stderr=subprocess.DEVNULL).decode() + return output.splitlines() + except subprocess.CalledProcessError: + print("Error: 'brew bundle dump' failed. Is homebrew installed?", file=sys.stderr) + sys.exit(1) + +def parse_brewfile(content): + """ + Parses Brewfile content. + Returns: + - conditional_pkgs: set of (type, name) + - preserved_footer: string (everything from first conditional onwards) + """ + lines = content.splitlines() + conditional_pkgs = set() + + # Find the start of the first conditional block + first_conditional_idx = -1 + in_conditional = 0 + + for i, line in enumerate(lines): + stripped = line.strip() + if stripped.startswith(('if ', 'unless ', 'case ')) and not stripped.endswith('; end'): + if first_conditional_idx == -1: + # Look back for comments that might belong to this block + j = i - 1 + while j >= 0 and (lines[j].strip().startswith('#') or not lines[j].strip()): + j -= 1 + first_conditional_idx = j + 1 + in_conditional += 1 + + if in_conditional > 0: + match = PKG_RE.match(line) + if match: + conditional_pkgs.add((match.group(1), match.group(2))) + + if stripped == 'end' or stripped.endswith('; end'): + in_conditional -= 1 + + if first_conditional_idx == -1: + return set(), "" + + footer = "\n".join(lines[first_conditional_idx:]) + return conditional_pkgs, footer + +def main(args): + repo_root = get_repo_root() + brewfile_path = os.path.join(repo_root, 'Brewfile') + + if not os.path.exists(brewfile_path): + print(f"Error: Brewfile not found at {brewfile_path}", file=sys.stderr) + sys.exit(1) + + with open(brewfile_path) as f: + old_content = f.read() + + conditional_pkgs, footer = parse_brewfile(old_content) + ignore_list = get_ignore_list(repo_root) + + dumped_lines = get_current_packages() + + new_unconditional_lines = [] + + for line in dumped_lines: + match = PKG_RE.match(line) + if match: + pkg_type, pkg_name = match.group(1), match.group(2) + if pkg_name in ignore_list: + continue + if (pkg_type, pkg_name) in conditional_pkgs: + continue + + # If it's not a package line (e.g. comment from dump), we can skip or keep + if line.strip(): + new_unconditional_lines.append(line) + + # Sort lines by type (tap, brew, cask, mas) then name + def sort_key(line): + match = PKG_RE.match(line) + if not match: return (4, line) + order = {'tap': 0, 'brew': 1, 'cask': 2, 'mas': 3} + return (order.get(match.group(1), 4), match.group(2)) + + new_unconditional_lines.sort(key=sort_key) + + # Build new content + new_content = "\n".join(new_unconditional_lines) + if footer: + if new_unconditional_lines: + new_content += "\n\n" + new_content += footer.strip() + "\n" + else: + new_content += "\n" + + if new_content == old_content: + print("Brewfile is already up to date.") + else: + if args.dry_run: + print("Changes detected (dry run):") + diff = difflib.unified_diff( + old_content.splitlines(keepends=True), + new_content.splitlines(keepends=True), + fromfile='Brewfile (original)', + tofile='Brewfile (new)' + ) + sys.stdout.writelines(diff) + else: + with open(brewfile_path, 'w') as f: + f.write(new_content) + print("Brewfile updated.") + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Update Brewfile while preserving conditionals.") + parser.add_argument("--dry-run", action="store_true", help="Show changes without applying them.") + args = parser.parse_args() + main(args) diff --git a/install.sh b/install.sh index 657d86f..eb61988 100755 --- a/install.sh +++ b/install.sh @@ -225,6 +225,13 @@ install_main() { } install_dotfiles link_directory_contents "${BASEDIR}/bin" "${HOME}/bin" "" + + # macOS specific Homebrew bundle installation + if [[ "$(uname)" == "Darwin" ]] && have_command brew && [[ -f "${BASEDIR}/Brewfile" ]]; then + verbose "Checking Homebrew bundle..." + brew bundle install --file="${BASEDIR}/Brewfile" --no-lock + fi + [[ "$INSTALL_KEYS" = 1 ]] && install_keys save_prefs cleanup