Skip to content

Commit a6f9589

Browse files
authored
Linux support (#1)
* feat: detect the platform and dispatch per os The installer hardcoded macos/install and macos/update, so a Linux run went straight into fdesetup, defaults and softwareupdate. utils/os detects the system once and the scripts dispatch to the matching topic instead. Constants that differ per platform (projects directory, font directory, the iCloud store) move here too, and force_link replaces the BSD-only 'ln -Fs' and 'ln -sfh' spellings, which GNU ln rejects. * feat: add a linux topic Linux is a headless configuration target, not a provisioning one: nothing here calls a package manager or asks for sudo. linux/tooling installs the few tools the other topics depend on into $HOME (fnm, uv, starship), linux/install reports the system packages that are missing with the exact install line, and linux/aliases.zsh and linux/functions.zsh are the counterparts to the macOS ones β€” clipboard() for pbcopy, notify-send for terminal-notifier, resolvectl for dscacheutil. Deliberately no 'ip' alias: on Linux that is iproute2's own command. * fix: skip the macOS-only topics on linux Homebrew, iTerm2, Zed, fonts and the iCloud-synced Claude state all guard themselves now and report a skip rather than failing. The Claude one mattered most: with no iCloud Drive present it would create a fake 'Library/Mobile Documents' tree on a Linux box and then move the real ~/.claude into it. The private-file linking in system/install had the same blind spot and warned once per file instead of once. tmux keeps working β€” TPM and the sidebar patches are portable β€” only the launchd dark-mode watcher is macOS-bound. tmux.conf falls back to DOTFILES_THEME since 'defaults' does not exist to read the appearance from. * fix: make the shell config load on linux Two unguarded evals ran on every shell start: '/opt/homebrew/bin/brew shellenv' in system/paths.zsh and 'pyenv init -' in python/paths.zsh. Neither exists on a Linux box, so both printed an error before the prompt appeared. The brew prefix is probed now, and the pyenv eval is dropped β€” system/paths.zsh already does a cached init, so it was running twice on macOS as well. The **/aliases.zsh glob also pulled in macos/aliases.zsh regardless of platform, shadowing working aliases with pbcopy and defaults calls. All three loaders skip the other platform's topic now, and the macOS-only entries moved out of system/aliases.zsh into macos/ where the loader can exclude them. Also fixes three things this uncovered: - CODE_DIR was only ever set in utils/constants, which the shell never sources, so $PROJECTS and $PROJECT_HOME were both silently empty - the dclint and 8000 aliases were double-quoted, so $(pwd) and the lsof lookup were expanded once at load time and baked in - copyssh still pointed at id_rsa.pub, while git/install generates ed25519 * fix: make git work off macOS gitconfig pinned gpg and gh to /opt/homebrew, so with commit.gpgsign on every commit failed on a machine without Homebrew, and the GitHub credential helper went with it. Both resolve from $PATH now. git/install picked osxkeychain, --apple-use-keychain, UseKeychain and pinentry-mac unconditionally β€” OpenSSH rejects the keychain options outright elsewhere. Each has a Linux branch now: libsecret when the distribution built it, the in-memory cache otherwise (never 'store', which writes credentials to disk in plain text), and pinentry-curses so signing still works over ssh. The devguard includeIf gained a ~/code counterpart, since the projects directory differs per platform. * fix: stop one missing tool aborting the whole run Under 'set -e' several topics took the entire installer down when a tool was absent, which is the normal state on a box these dotfiles do not provision: - node and python called fail_hard on a missing fnm or uv - zsh/update ran 'sh /tools/upgrade.sh' when $ZSH was unset, i.e. on any machine that had not been through the installer yet - zsh/install ran the oh-my-zsh installer, which bails when zsh is missing - herdr aborted when the installer had no build for the platform All of them warn and skip now. vim/install also asks for neovim explicitly and installs plugins with nvim rather than vim β€” the autoload path it writes to is neovim's, so plain vim reported success without ever loading plug.vim. * chore: document linux support and soften the linter The README claimed macOS or Linux while the code only ever ran on macOS. It now describes what Linux actually gets: configuration plus $HOME-local tooling, no package manager, no sudo, with the missing packages reported at the end. scripts/lint skips zsh and ruby when they are not installed instead of failing, matching how it already treats shellcheck β€” CI installs both explicitly, so coverage is unchanged. 'dotfiles -e' falls back through $VISUAL and $EDITOR to a terminal editor, since a headless box has no GUI editor to hand over to. * feat: install declared system packages on linux linux/packages is the counterpart to homebrew/Brewfile: a declared list of system packages, installed with apt when missing. It replaces the earlier report-only behaviour, so a fresh box reaches a working shell from 'dotfiles -i' alone rather than from a warning telling you what to type. Entries are 'probe:package' because the two halves cannot be derived from each other β€” nvim comes from neovim, delta from git-delta β€” and a probe may be a file rather than a command, which is how the two zsh plugins are detected. Alternatives separated by '|' cover the Debian-renamed binaries (bat/batcat, fd/fdfind). Packages install from linux/tooling, early enough that zsh/install and vim/install find what they need, and 'dotfiles -u' picks up newly declared entries the way 'brew bundle' does. The update also upgrades installed packages, mirroring brew upgrade and softwareupdate on the mac. Sudo is requested only when something is actually missing, so the usual run never prompts, and a machine without sudo reports what to run instead of failing. unzip is declared because fnm's installer refuses to run without it, which is how it was found. * fix: let ~/.gitconfig.local actually own the identity Two bugs that only show on a fresh machine, found setting this up on linux. create_symlinks runs from system/install, several topics before git/install generates gitconfig.local.symlink β€” so on a first run the file did not exist yet when the symlinker looked, ~/.gitconfig.local was never created, and the [include] resolved to nothing. git/install links the file it generates now. Even with the link in place the identity was ignored: gitconfig.symlink has the [include] at the top but carried a hardcoded [user] block near the bottom, and in git the later value wins. So the name and email entered at the prompt were overridden by the committed ones, and a personal address shipped in a public repository for no benefit. The block is gone; the per-machine file is the only thing that sets an identity, which is what the installer generating it implied all along. github.user also took AUTHORNAME, writing a person's name where tooling wants an account name, so it is prompted for separately. * fix: report when the installer finishes The closing message was commented out, so 'dotfiles -i' ended on whatever the last topic happened to print and read as though it had stopped partway. The updater has always said when it was done β€” this makes the two match. * fix: run the oh-my-zsh upgrade with zsh, not sh upgrade.sh declares '#!/usr/bin/env zsh' and uses 'local' at the top level, but it was being invoked through 'sh'. On macOS that is bash, which tolerates it; on debian it is dash, which rejects it with 'local: not in a function' and exits 2. Since the output went to /dev/null and 'set -e' was on, every 'dotfiles -u' on linux died silently at 'Update [zsh]' β€” two topics in, with node, python, tmux, herdr and the platform update never running at all. Now invoked with zsh, and a failure warns and points at the script instead of ending the run. vim/update gets the same treatment: a plugin that fails to fetch should not take everything after it down either.
1 parent 49f3a41 commit a6f9589

46 files changed

Lines changed: 907 additions & 120 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

β€ŽREADME.mdβ€Ž

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,33 @@ fresh installation is recommended. Use at your own risk.
2222
The installer asks for your admin password once up front β€” Rosetta, Homebrew
2323
casks, FileVault and the `/etc/hosts` symlink all need it later on.
2424

25+
**Linux notes**: Linux is headless β€” no GUI applications, no casks β€” but it
26+
does provision its own command line. `linux/packages` is the counterpart to the
27+
Brewfile: a declared list of system packages, installed with `apt` when
28+
missing. Tools with no distribution package worth using (`fnm`, `uv`,
29+
`starship`) are installed into `$HOME` instead, alongside oh-my-zsh, TPM,
30+
vim-plug and herdr.
31+
32+
Sudo is asked for **only when a package is actually missing**, which is the
33+
first run and nothing after it. Where sudo is unavailable β€” a container, or a
34+
box you do not administer β€” nothing fails: the run reports what is missing and
35+
carries on.
36+
37+
[WARN] Still missing: neovim git-delta
38+
[ .. ] sudo apt install neovim git-delta
39+
40+
`dotfiles -u` also upgrades installed packages, mirroring what `brew upgrade`
41+
and `softwareupdate` already do on the mac. That block sits on its own in
42+
`linux/update` and is easy to remove if you would rather own upgrades
43+
yourself; it is skipped silently when there is no cached sudo.
44+
45+
Only `apt` is implemented. Other package managers get the report and the
46+
matching install line, not an automatic install.
47+
48+
Applications, fonts, iTerm2, the Dock and the iCloud-synced config are macOS
49+
concerns and skip themselves with a message. If you have no iCloud Drive,
50+
machine-local secrets go in `~/.localrc` as usual.
51+
2552
The following command will install the dotfiles into `~/.dotfiles` and runs the
2653
installer automatically πŸ€–:
2754

@@ -58,7 +85,16 @@ of that:
5885
- every `.symlink` file in a topic will be mapped to `~/.[filename]`
5986
- everything in the `bin/` folder gets automatically added to your `$PATH`
6087

61-
The following package flavours are installed:
88+
`macos/` and `linux/` are the two **platform topics**, and exactly one of them
89+
is ever active: `utils/os` detects the system, `scripts/install` dispatches to
90+
the matching topic, and the shell loaders skip the other one's `aliases.zsh`
91+
and `functions.zsh`. That is what keeps `pbcopy` and `defaults` out of a Linux
92+
shell, and `resolvectl` out of a mac. Topics that only make sense on one side
93+
(Homebrew, iTerm2, fonts, the iCloud sync) guard themselves and report a skip
94+
rather than being left out of the list.
95+
96+
The macOS package list lives in `homebrew/Brewfile`, the Linux one in
97+
`linux/packages`. The following package flavours are installed **on macOS**:
6298

6399
- [FiraCode](https://github.com/tonsky/FiraCode) with nice custom font management
64100
- [Git with GPG signing](https://gnupg.org/) enabled, plus [delta](https://github.com/dandavison/delta) as the diff pager

β€Žbin/dotfilesβ€Ž

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,16 @@ while test $# -gt 0; do
3636
exit
3737
;;
3838
"-e"|"--edit")
39-
exec "code" "$DIR"
40-
exit
39+
# `code` is the GUI editor launcher on the mac. a headless linux box has
40+
# none, so fall back through $VISUAL/$EDITOR to a terminal editor rather
41+
# than failing with "command not found"
42+
for editor in code zed "$VISUAL" "$EDITOR" nvim vim; do
43+
if [ -n "$editor" ] && type "$editor" > /dev/null 2>&1; then
44+
exec "$editor" "$DIR"
45+
fi
46+
done
47+
echo "No editor found, set \$EDITOR or \$VISUAL"
48+
exit 1
4149
;;
4250
"-g"|"--go")
4351
cd $DIR

β€Žclaude/installβ€Ž

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@ source "$DIR/utils/index"
33

44
info "Setup \033[0;37m[claude]\033[0m"
55

6-
SYSTEM_DIR="$HOME/Library/Mobile Documents/com~apple~CloudDocs/System"
6+
# this topic syncs agent state through iCloud Drive, which only exists on
7+
# macOS. without this guard the `mkdir` below would happily create a fake
8+
# iCloud tree on a linux box and then *move* the real ~/.claude into it
9+
if ! is_macos; then
10+
success "No iCloud Drive on $DOTFILES_OS, agent state stays local"
11+
return 0
12+
fi
13+
714
mkdir -p "$SYSTEM_DIR"
815

916
# keep agent state in iCloud, symlinked into place so both machines share

β€Žclaude/updateβ€Ž

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ source "$DIR/utils/index"
33

44
info "Update \033[0;37m[claude]\033[0m"
55

6-
SYSTEM_DIR="$HOME/Library/Mobile Documents/com~apple~CloudDocs/System"
6+
# see claude/install β€” the iCloud backup is macOS only
7+
if ! is_macos; then
8+
success "No iCloud Drive on $DOTFILES_OS, nothing to back up"
9+
return 0
10+
fi
711

812
if [ -f "$HOME/.claude.json" ] && [ ! -L "$HOME/.claude.json" ]; then
913
cp "$HOME/.claude.json" "$SYSTEM_DIR/.claude.json.backup"

β€Žfonts/installβ€Ž

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,21 @@ source "$DIR/utils/index"
33

44
info "Setup \033[0;37m[fonts]\033[0m"
55

6+
# fonts are rendered by whatever terminal or browser you are sitting in front
7+
# of. on a headless linux box that is a different machine entirely, so there
8+
# is nothing to install into β€” fc-cache is the tell that a desktop is present
9+
if ! is_macos && ! is_executable "fc-cache"; then
10+
success "No desktop font stack present, skipped"
11+
return 0
12+
fi
13+
614
# setup variables
715
FONTS_DIR="$DIR/fonts"
816
FONTS_FILE="$FONTS_DIR/fonts.txt"
9-
system_fonts_dir="$HOME/Library/Fonts"
17+
# per-user font directory, ~/Library/Fonts on macOS and the XDG data dir on
18+
# linux (see utils/constants)
19+
system_fonts_dir="$FONTS_INSTALL_DIR"
20+
mkdir -p "$system_fonts_dir"
1021

1122
# specificly copy fonts files defined in FONTS_FILE
1223
while read font; do

β€Žgit/gitconfig.local.symlink.exampleβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
signingkey = AUTHOREMAIL
55

66
[github]
7-
user = AUTHORNAME
7+
user = GITHUBUSER
88

99
[credential]
1010
helper = GIT_CREDENTIAL_HELPER

β€Žgit/gitconfig.symlinkβ€Ž

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,9 @@
130130
gpgsign = true
131131

132132
[gpg]
133-
program = /opt/homebrew/bin/gpg
133+
# resolved from $PATH rather than pinned to /opt/homebrew β€” the same
134+
# config now has to work on linux, where homebrew is not installed
135+
program = gpg
134136

135137
[diff]
136138
# [i]ndex, [w]ork tree, [c]ommit, [o]bject instead of a/b
@@ -186,9 +188,12 @@
186188
smudge = git-lfs smudge -- %f
187189
process = git-lfs filter-process
188190
required = true
189-
[user]
190-
name = Angelo Dini
191-
email = angelo.dini@divio.ch
191+
# no [user] block here on purpose: the identity is per machine and lives in
192+
# ~/.gitconfig.local, which git/install generates and the [include] at the top
193+
# of this file pulls in. A [user] block here would appear later in the file
194+
# than that include and silently win, which is exactly what it used to do β€”
195+
# and it put a personal address in a public repository besides.
196+
192197
[safe]
193198
directory = ~/Sites/actions-runner/_work/devguard/devguard
194199

@@ -205,17 +210,22 @@
205210
# let the gh CLI handle HTTPS GitHub credentials
206211
[credential "https://github.com"]
207212
helper =
208-
helper = !/opt/homebrew/bin/gh auth git-credential
213+
helper = !gh auth git-credential
209214

210215
[credential "https://gist.github.com"]
211216
helper =
212-
helper = !/opt/homebrew/bin/gh auth git-credential
217+
helper = !gh auth git-credential
213218

214219
[tag]
215220
# sort tags as version numbers
216221
sort = version:refname
217222

218-
# devguard identity for all repos under ~/Sites/devguard/
219-
# (must stay last so it overrides [user] above and ~/.gitconfig.local)
223+
# devguard identity for all repos under the projects directory
224+
# (must stay last so it overrides [user] above and ~/.gitconfig.local).
225+
# both roots are listed because the projects directory differs per platform:
226+
# ~/Sites on macOS, ~/code on linux β€” a missing include is silently ignored
220227
[includeIf "gitdir:~/Sites/devguard/"]
221228
path = ~/.gitconfig-devguard
229+
230+
[includeIf "gitdir:~/code/devguard/"]
231+
path = ~/.gitconfig-devguard

β€Žgit/installβ€Ž

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,17 @@ if [ ! -f "$SSH_DIR/id_ed25519.pub" ] && [ ! -f "$SSH_DIR/id_rsa.pub" ]; then
2323
user "Copy the SSH public key to github/gitlab! Enter to continue."
2424
read -e _
2525

26-
# register key
27-
touch ~/.ssh/config
28-
echo -e "Host *\n AddKeysToAgent yes\n IgnoreUnknown AddKeysToAgent,UseKeychain\n UseKeychain yes\n IdentityFile $SSH_DIR/id_ed25519" >> $SSH_DIR/config
29-
30-
ssh-add --apple-use-keychain $SSH_DIR/id_ed25519 > /dev/null
26+
# register key. UseKeychain and --apple-use-keychain store the passphrase in
27+
# the macOS keychain and are rejected outright by OpenSSH elsewhere, so the
28+
# linux config is the same block without them
29+
touch "$SSH_DIR/config"
30+
if is_macos; then
31+
echo -e "Host *\n AddKeysToAgent yes\n IgnoreUnknown AddKeysToAgent,UseKeychain\n UseKeychain yes\n IdentityFile $SSH_DIR/id_ed25519" >> $SSH_DIR/config
32+
ssh-add --apple-use-keychain $SSH_DIR/id_ed25519 > /dev/null
33+
else
34+
echo -e "Host *\n AddKeysToAgent yes\n IdentityFile $SSH_DIR/id_ed25519" >> $SSH_DIR/config
35+
ssh-add $SSH_DIR/id_ed25519 > /dev/null
36+
fi
3137

3238
success "SSH key successfully generated in $SSH_DIR"
3339
else
@@ -36,23 +42,47 @@ fi
3642

3743
# setup gitconfig
3844
if ! [ -f "$DIR/git/gitconfig.local.symlink" ]; then
39-
git_credential="osxkeychain"
45+
# osxkeychain ships with the macOS build of git and does not exist elsewhere.
46+
# libsecret is the linux equivalent when the distribution built it, otherwise
47+
# fall back to the in-memory cache β€” never `store`, which writes the
48+
# credentials to disk in plain text
49+
if is_macos; then
50+
git_credential="osxkeychain"
51+
elif git credential-libsecret --help > /dev/null 2>&1; then
52+
git_credential="libsecret"
53+
else
54+
git_credential="cache --timeout=3600"
55+
fi
4056

4157
# need git information
4258
user "Enter your git author name:"
4359
read -e git_authorname
4460
user "Enter your git author email:"
4561
read -e git_authoremail
62+
# github.user is the account name, not the person's name β€” tooling that reads
63+
# it uses it to build api and clone urls
64+
user "Enter your github username:"
65+
read -e git_githubuser
4666

47-
sed -e "s/AUTHORNAME/$git_authorname/g" -e "s/AUTHOREMAIL/$git_authoremail/g" -e "s/GIT_CREDENTIAL_HELPER/$git_credential/g" "$DIR/git/gitconfig.local.symlink.example" > "$DIR/git/gitconfig.local.symlink"
67+
sed -e "s/AUTHORNAME/$git_authorname/g" -e "s/AUTHOREMAIL/$git_authoremail/g" -e "s/GITHUBUSER/$git_githubuser/g" -e "s|GIT_CREDENTIAL_HELPER|$git_credential|g" "$DIR/git/gitconfig.local.symlink.example" > "$DIR/git/gitconfig.local.symlink"
4868

4969
success "Git successfully configured"
5070
else
5171
success "Git already configured, remove ~/.dotfiles/gitconfig.local.symlink to recreate"
5272
fi
5373

74+
# link it here rather than leaving it to create_symlinks. That runs from
75+
# system/install, several topics earlier, so on a fresh machine the file does
76+
# not exist yet when it looks β€” ~/.gitconfig.local was then never created, the
77+
# [include] in gitconfig resolved to nothing, and the identity just entered
78+
# above was silently ignored until the next run.
79+
force_link "$DIR/git/gitconfig.local.symlink" "$HOME/.gitconfig.local"
80+
success "Linked ~/.gitconfig.local"
81+
5482
# setup GPG for signing git commits
55-
if [ ! -d "$HOME/.gnupg" ]; then
83+
if ! is_executable "gpg"; then
84+
warn "gpg is not installed, commit signing skipped β€” run '$(install_hint gnupg)'"
85+
elif [ ! -d "$HOME/.gnupg" ]; then
5686
user "Generating GPG key, use same email as for git:"
5787
# generate key, prompts will appear
5888
gpg --default-new-key-algo rsa4096 --gen-key
@@ -62,9 +92,16 @@ if [ ! -d "$HOME/.gnupg" ]; then
6292
user "Copy the GPG public key to github/gitlab! Enter to continue."
6393
read -e _
6494

65-
# install and restart config
95+
# install and restart config. pinentry-mac is a macOS program; on linux the
96+
# curses prompt is the equivalent that works over ssh, with the plain tty one
97+
# as a fallback
6698
git config --global gpg.program "$(which gpg)"
67-
echo "pinentry-program $(which pinentry-mac)" >> "$HOME/.gnupg/gpg-agent.conf"
99+
for pinentry in pinentry-mac pinentry-curses pinentry-tty pinentry; do
100+
if is_executable "$pinentry"; then
101+
echo "pinentry-program $(which $pinentry)" >> "$HOME/.gnupg/gpg-agent.conf"
102+
break
103+
fi
104+
done
68105
gpgconf --kill gpg-agent
69106

70107
success "GPG key successfully setup"

β€Žherdr/installβ€Ž

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ info "Setup \033[0;37m[herdr]\033[0m"
88
if [ -x "${HERDR_INSTALL_DIR:-$HOME/.local/bin}/herdr" ]; then
99
# already there, the updater pulls the latest release
1010
source "$DIR/herdr/update"
11-
else
12-
curl -fsSL https://herdr.dev/install.sh | sh > /dev/null 2>&1
11+
elif curl -fsSL https://herdr.dev/install.sh | sh > /dev/null 2>&1; then
1312
success "Herdr successfully installed"
13+
else
14+
# the installer resolves a release per os/arch β€” a machine it has no build
15+
# for used to abort the entire run under `set -e`
16+
warn "Could not install herdr, see https://herdr.dev"
1417
fi

β€Žherdr/updateβ€Ž

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,8 @@ info "Update \033[0;37m[herdr]\033[0m"
66
# the official installer is idempotent: it resolves the latest release from the
77
# same manifest `herdr update` uses, verifies the checksum and replaces the
88
# binary β€” so re-running it is the update
9-
curl -fsSL https://herdr.dev/install.sh | sh > /dev/null 2>&1
10-
success "Herdr up to date"
9+
if curl -fsSL https://herdr.dev/install.sh | sh > /dev/null 2>&1; then
10+
success "Herdr up to date"
11+
else
12+
warn "Could not update herdr, see https://herdr.dev"
13+
fi

0 commit comments

Comments
Β (0)