Profiles: machines, roles and shared layers
The repo was one machine's capture. It is now a set of profiles built from layers under profiles/: machines/ hp-laptop, macbook-air-m2, coder-desktop roles/ developer, devops, ai-agents features/ handoff (lan-mouse) shared base, personal, personal-desktop A profile resolves to its includes (depth first), any --roles, then itself; for a path several layers provide, the last wins. install.sh --show prints the layers and which one owns each file. Hyprland settings no longer share a file: base's hyprland.lua loads ~/.config/hypr/layers/*.lua with Omarchy's own require_all, and bindings.lua stays stock so nothing binds twice. Verified with Hyprland 0.56.2 --verify-config for all three profiles, plus a broken probe layer to prove the directory is loaded. packages.txt gains per-line arch (Arch Linux ARM lacks opencode, codex, mise, shellcheck) and cmd= (skip when dotfiles' ~/.local/bin already provides the command), and an [ignore] section that replaces capture.sh's hard-coded list. Tab-separated records would have collapsed empty fields under read, so package records use the unit separator. hp-laptop: re-captured. monitors.lua catch-all scale is now 1, shell.json has four new plugins, fix-stuck-displays has its connector-name fix, navbar-cat is gone, plugin-manager and omarchy-phone are pinned, the herdr-hud working tree is re-diffed and bin/herdr-web rides along. Its handoff peer is now the Air (192.168.1.49); the old default list was the HP's own addresses, written from the Air's side. macbook-air-m2: captured read-only over SSH as it is (no themes, plugins or bar changes). Its LAN_MOUSE_POINTER_SCALE=0.7 moves from the shared unit into a lan-mouse.service.d drop-in in its own layer. coder-desktop: the Omarchy settings covert-cloud's image used to patch in by hand (Virtual-1 at 1920x1080, no animations, stay-awake, Retro 82 once), for the image to apply at a pinned commit. Roles reach it per workspace via install.sh --roles ... --no-clobber. capture.sh writes each file back into the layer that owns it, and refuses to run before the first layered install unless --force, since the old file shapes would land in shared layers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0153egowV1Xw2RNjZnSyZicc
This commit is contained in:
+183
@@ -0,0 +1,183 @@
|
||||
# shellcheck shell=bash disable=SC1090
|
||||
# Profile resolution, shared by install.sh and capture.sh. Sourced, not run.
|
||||
#
|
||||
# A profile is a directory under profiles/ with a `profile` file (bash, sourced)
|
||||
# and any of: config/ (mirrors ~/.config), themes/, plugins.txt,
|
||||
# plugin-patches/, bin/, systemd/user/, system/ (mirrors /etc), handoff/,
|
||||
# packages.txt, setup.sh. `include=(...)` names the layers it builds on.
|
||||
#
|
||||
# Resolving a profile gives an ordered list of layers: each layer's includes
|
||||
# first (depth first, each layer once), then any --roles, then the profile
|
||||
# itself. For a path that several layers provide, the LAST layer wins, so a
|
||||
# machine always overrides the roles and shared layers under it.
|
||||
#
|
||||
# Expects REPO to be set, and err/warn helpers from the caller.
|
||||
|
||||
PROFILES=$REPO/profiles
|
||||
STATE_FILE=$HOME/.config/omarchy-setup/profile
|
||||
|
||||
LAYERS=() # resolved layer paths relative to profiles/, in order
|
||||
PROFILE="" # the profile asked for, e.g. machines/hp-laptop
|
||||
ROLES=() # extra roles asked for, e.g. devops ai-agents
|
||||
_resolving=()
|
||||
|
||||
# find_layer <name> -> layer path relative to profiles/. Accepts the full path
|
||||
# (machines/hp-laptop) or a bare name looked up in machines/, roles/, features/.
|
||||
find_layer() {
|
||||
local c
|
||||
for c in "$1" "machines/$1" "roles/$1" "features/$1"; do
|
||||
[[ -f $PROFILES/$c/profile ]] && { printf '%s\n' "$c"; return 0; }
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
layer_includes() {
|
||||
( include=(); source "$PROFILES/$1/profile"; printf '%s\n' "${include[@]}" )
|
||||
}
|
||||
|
||||
layer_description() {
|
||||
( description=""; source "$PROFILES/$1/profile"; printf '%s\n' "$description" )
|
||||
}
|
||||
|
||||
_add_layer() {
|
||||
local rel l inc
|
||||
rel=$(find_layer "$1") || { err "no such layer: $1"; return 1; }
|
||||
for l in "${LAYERS[@]}"; do [[ $l == "$rel" ]] && return 0; done
|
||||
[[ " ${_resolving[*]} " == *" $rel "* ]] && { err "include cycle through $rel"; return 1; }
|
||||
_resolving+=("$rel")
|
||||
while IFS= read -r inc; do
|
||||
[[ -n $inc ]] || continue
|
||||
_add_layer "$inc" || return 1
|
||||
done < <(layer_includes "$rel")
|
||||
LAYERS+=("$rel")
|
||||
}
|
||||
|
||||
# resolve <profile> [role...] -> fills LAYERS
|
||||
resolve() {
|
||||
local rel inc r
|
||||
LAYERS=(); _resolving=()
|
||||
rel=$(find_layer "$1") || { err "no such profile: $1 (./install.sh --profiles lists them)"; return 1; }
|
||||
PROFILE=$rel
|
||||
_resolving+=("$rel")
|
||||
while IFS= read -r inc; do
|
||||
[[ -n $inc ]] || continue
|
||||
_add_layer "$inc" || return 1
|
||||
done < <(layer_includes "$rel")
|
||||
for r in "${@:2}"; do _add_layer "roles/${r#roles/}" || return 1; done
|
||||
LAYERS+=("$rel")
|
||||
}
|
||||
|
||||
# Settings from the layers' profile files. Arrays accumulate across layers;
|
||||
# handoff_peer_ips and arch come from the last layer that sets them.
|
||||
P_ARCH=""; P_HOSTNAMES=(); P_HANDOFF_IPS=(); P_DOTFILES=(); P_AETHER=()
|
||||
load_settings() {
|
||||
local l
|
||||
P_ARCH=""; P_HOSTNAMES=(); P_HANDOFF_IPS=(); P_DOTFILES=(); P_AETHER=()
|
||||
for l in "${LAYERS[@]}"; do
|
||||
local arch="" hostnames=() handoff_peer_ips=() dotfiles=() aether_themes=() include=() description=""
|
||||
source "$PROFILES/$l/profile"
|
||||
[[ -n $arch ]] && P_ARCH=$arch
|
||||
(( ${#hostnames[@]} )) && P_HOSTNAMES=("${hostnames[@]}")
|
||||
(( ${#handoff_peer_ips[@]} )) && P_HANDOFF_IPS=("${handoff_peer_ips[@]}")
|
||||
P_DOTFILES+=("${dotfiles[@]}")
|
||||
P_AETHER+=("${aether_themes[@]}")
|
||||
done
|
||||
}
|
||||
|
||||
# Which profile to use when none is given: the one recorded by the last
|
||||
# install on this machine, else the machine profile whose hostnames= matches.
|
||||
# Prints "profile<TAB>role,role".
|
||||
default_profile() {
|
||||
if [[ -f $STATE_FILE ]]; then
|
||||
local profile="" roles=""
|
||||
source "$STATE_FILE"
|
||||
[[ -n $profile ]] && { printf '%s\t%s\n' "$profile" "$roles"; return 0; }
|
||||
fi
|
||||
local f host; host=$(hostname 2>/dev/null || cat /etc/hostname)
|
||||
for f in "$PROFILES"/machines/*/profile; do
|
||||
if ( hostnames=(); source "$f"; [[ " ${hostnames[*]} " == *" $host "* ]] ); then
|
||||
f=${f%/profile}; printf '%s\t\n' "${f#"$PROFILES/"}"; return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
save_state() {
|
||||
mkdir -p "${STATE_FILE%/*}"
|
||||
local IFS=,
|
||||
printf '# Written by omarchy-setup/install.sh; read when no --profile is given.\nprofile=%q\nroles=%q\n' \
|
||||
"$PROFILE" "${ROLES[*]}" > "$STATE_FILE"
|
||||
}
|
||||
|
||||
# owners <subdir> -> "relpath<TAB>layer" for every file under <layer>/<subdir>
|
||||
# across the layers, the last layer winning each relpath. Sorted by relpath.
|
||||
owners() {
|
||||
local sub=$1 l f
|
||||
declare -A own=()
|
||||
for l in "${LAYERS[@]}"; do
|
||||
[[ -d $PROFILES/$l/$sub ]] || continue
|
||||
while IFS= read -r -d '' f; do
|
||||
own[${f#"$PROFILES/$l/$sub/"}]=$l
|
||||
done < <(find "$PROFILES/$l/$sub" -type f -print0)
|
||||
done
|
||||
for f in "${!own[@]}"; do printf '%s\t%s\n' "$f" "${own[$f]}"; done | sort
|
||||
}
|
||||
|
||||
# Every layer that has <path> (a file or directory), in order.
|
||||
layers_with() {
|
||||
local l
|
||||
for l in "${LAYERS[@]}"; do [[ -e $PROFILES/$l/$1 ]] && printf '%s\n' "$l"; done
|
||||
}
|
||||
|
||||
# Parse every layer's packages.txt into PKG_<section> arrays of
|
||||
# "name<US>archs<US>cmds<US>layer", US being $PKG_FS. Not a tab: tab is IFS
|
||||
# whitespace, so `read` would collapse an empty archs field and shift the rest.
|
||||
# [ignore] lines may hold several names.
|
||||
PKG_FS=$'\x1f'
|
||||
PKG_repo=(); PKG_aur=(); PKG_optional=(); PKG_ignore=()
|
||||
load_packages() {
|
||||
local l line section tok name archs cmds
|
||||
PKG_repo=(); PKG_aur=(); PKG_optional=(); PKG_ignore=()
|
||||
declare -A seen=()
|
||||
for l in "${LAYERS[@]}"; do
|
||||
[[ -f $PROFILES/$l/packages.txt ]] || continue
|
||||
section=""
|
||||
while IFS= read -r line; do
|
||||
line=${line%%#*}
|
||||
read -ra tok <<<"$line"
|
||||
(( ${#tok[@]} )) || continue
|
||||
case ${tok[0]} in
|
||||
'[repo]'|'[aur]'|'[optional]'|'[ignore]') section=${tok[0]//[\[\]]/}; continue ;;
|
||||
esac
|
||||
[[ -n $section ]] || continue
|
||||
if [[ $section == ignore ]]; then
|
||||
for name in "${tok[@]}"; do PKG_ignore+=("$name$PKG_FS$PKG_FS$PKG_FS$l"); done
|
||||
continue
|
||||
fi
|
||||
name=${tok[0]}; archs=""; cmds=""
|
||||
for t in "${tok[@]:1}"; do
|
||||
case $t in
|
||||
cmd=*) cmds=${t#cmd=} ;;
|
||||
x86_64|aarch64) archs+="$t," ;;
|
||||
*) warn "$l/packages.txt: unknown token '$t' on $name" ;;
|
||||
esac
|
||||
done
|
||||
[[ -n ${seen[$name]:-} ]] && continue
|
||||
seen[$name]=1
|
||||
local -n arr="PKG_$section"
|
||||
arr+=("$name$PKG_FS${archs%,}$PKG_FS$cmds$PKG_FS$l")
|
||||
unset -n arr
|
||||
done < "$PROFILES/$l/packages.txt"
|
||||
done
|
||||
}
|
||||
|
||||
# pkg_applies <archs> -> true when this machine's arch is in the list (or no list)
|
||||
pkg_applies() { [[ -z $1 || ",$1," == *",$(uname -m),"* ]]; }
|
||||
|
||||
# pkg_cmd_present <cmds> -> true when any of the comma-separated commands exists
|
||||
pkg_cmd_present() {
|
||||
local c
|
||||
[[ -n $1 ]] || return 1
|
||||
for c in ${1//,/ }; do command -v "$c" >/dev/null 2>&1 && return 0; done
|
||||
return 1
|
||||
}
|
||||
Reference in New Issue
Block a user