This commit is contained in:
David Tomaschik
2026-03-25 08:25:03 -07:00
parent 829f7ae1de
commit b6af18017b
8 changed files with 246 additions and 21 deletions

6
.Brewfile.ignore Normal file
View File

@@ -0,0 +1,6 @@
# Add package names here to ignore them in Brewfile updates.
# One package per line.
# Example:
# iterm2
# wget
orbstack

1
.githooks/pre-commit Symbolic link
View File

@@ -0,0 +1 @@
githooks.sh

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 ###

156
bin/macos/update_brewfile Executable file
View File

@@ -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)

View File

@@ -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