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 ## Project Structure
* `bin/`: Contains executable scripts that should be available in the shell's `PATH`. * `bin/`: Contains executable scripts symlinked to `~/bin/`. Subdirectories like `macos/`, `restic/`, and `setup/` are included.
* `dotfiles/`: Contains configuration files (dotfiles) to be symlinked into the home directory. * `dotfiles/`: Contains configuration files (dotfiles) symlinked to 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. * `dotfile_overlays/`: Each directory within is symlinked to the home directory, allowing for modular or git-submodule-based configurations.
* `install.sh`: The main installation script that sets up the environment, symlinks dotfiles, and installs packages. * `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 ## Notes on Security Issues
@@ -43,18 +49,20 @@ If making changes that affects how the user installs the tools, update
### Adding a new dotfile ### Adding a new dotfile
1. Place the new dotfile in the `dotfiles/` directory. 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/` ### 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`). 2. Ensure the script is executable (`chmod +x`).
### Adding a new package ### Adding a new package
1. Identify the appropriate package list in the `packages/` directory (e.g., `packages/cli`, `packages/kali`). 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. 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 ### Platform-specific changes

View File

@@ -66,9 +66,7 @@ brew "zlib"
brew "zsh-syntax-highlighting" brew "zsh-syntax-highlighting"
brew "sass/sass/migrator" brew "sass/sass/migrator"
brew "sass/sass/sass" brew "sass/sass/sass"
cask "claude-code"
cask "codeql" cask "codeql"
cask "cryptomator"
cask "cyberduck" cask "cyberduck"
cask "font-fira-code-nerd-font" cask "font-fira-code-nerd-font"
cask "font-fira-mono-nerd-font" cask "font-fira-mono-nerd-font"
@@ -77,7 +75,6 @@ cask "font-hack-nerd-font"
cask "font-inconsolata-nerd-font" cask "font-inconsolata-nerd-font"
cask "font-symbols-only-nerd-font" cask "font-symbols-only-nerd-font"
cask "font-terminess-ttf-nerd-font" cask "font-terminess-ttf-nerd-font"
cask "gcloud-cli"
cask "ghidra" cask "ghidra"
cask "gimp" cask "gimp"
cask "github" cask "github"
@@ -85,7 +82,6 @@ cask "iterm2"
cask "macfuse" cask "macfuse"
cask "meld" cask "meld"
cask "mitmproxy" cask "mitmproxy"
cask "orbstack"
cask "raycast" cask "raycast"
cask "rectangle" cask "rectangle"
cask "scroll-reverser" cask "scroll-reverser"
@@ -93,9 +89,18 @@ cask "temurin"
cask "veracrypt" cask "veracrypt"
cask "zulu@17" 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 # non-corp
if ENV['USER'] != "davidtomaschik" if !is_corp?
brew "bazel" brew "bazel"
brew "openssh" brew "openssh"
cask "claude-code"
cask "cryptomator"
cask "gcloud-cli"
cask "google-cloud-sdk" cask "google-cloud-sdk"
cask "orbstack"
end end

View File

@@ -1,16 +1,24 @@
### About ### ### About ###
This is a repository of configuration files that I like to have on all the 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" machines that I use. For new systems, you can bootstrap by running the
and get most things setup the way I like them. 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, 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 options to install just parts of it, such as on a machine where I only have a
user account but no root. user account but no root.
This now uses [git-crypt](https://github.com/AGWA/git-crypt) to protect This environment supports using `dotfile_overlays/` or `local_dotfiles/` to
`private_dotfiles` for things I don't want to splash all over the internet. :) 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. I still wouldn't check in anything terribly sensitive, like private keys.
### Usefulness ### ### Usefulness ###
@@ -44,16 +52,30 @@ sudo apt-get install xbindkeys xdotool
After installation, the functionality will be enabled automatically on your After installation, the functionality will be enabled automatically on your
next login. 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 BASEDIR: Where the skel framework is installed. Defaults to $HOME/.skel
MINIMAL: Don't do things that require git clones or installation of anything 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 INSTALL_KEYS: Install GnuPG and SSH keys. SSH keys are placed in
authorized_keys. (Defaults to 1, installs keys.) authorized_keys. (Defaults to 1, installs keys.)
TRUST_ALL_KEYS: Allow all keys to be used for SSH login, versus a small subset. 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. VERBOSE: Enable verbose output during installation. (Defaults to 0)
(Defaults to opposite of $MINIMAL.) SAVE: Save the install options to ${BASEDIR}/.installed-prefs
SAVE: Save the install options to ${BASEDIR}/installed-prefs
``` ```
### TODO ### ### 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 install_dotfiles
link_directory_contents "${BASEDIR}/bin" "${HOME}/bin" "" 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 [[ "$INSTALL_KEYS" = 1 ]] && install_keys
save_prefs save_prefs
cleanup cleanup