#!/bin/bash # dprofile - install Das OS profiles: sets of packages from the Void # repository with their services and groups, plus optional applications from # Flathub. The profiles are in /usr/share/das/profiles/NAME.conf. set -euo pipefail # shellcheck source=helpers.sh . "${DAS_HELPERS:-/usr/lib/das/helpers.sh}" profiles_dir=${DAS_PROFILES:-/usr/share/das/profiles} flathub=https://dl.flathub.org/repo/flathub.flatpakrepo usage() { if [[ -n $uk ]]; then cat <<'EOF' Використання: dprofile КОМАНДА [ПРОФІЛЬ] [...] list профілі та їхній стан (типова команда) show ПРОФІЛЬ пакети, служби й додаткові застосунки профілю install ПРОФІЛЬ встановити профіль (зі знімком системи для відкату): -y, --yes без запитань --apps ID,...|all|none які застосунки з Flathub додати (інакше запитає) apps ПРОФІЛЬ додаткові застосунки профілю з Flathub app add ID... встановити застосунки з Flathub (ID або його кінець: AndroidStudio, GoLand) app rm ID... видалити їх Профіль dev: dprofile install dev EOF else cat <<'EOF' Usage: dprofile COMMAND [PROFILE] [...] list profiles and their state (the default) show PROFILE the profile's packages, services and optional apps install PROFILE install a profile (with a system snapshot to roll back to): -y, --yes ask nothing --apps ID,...|all|none which Flathub applications to add (asks otherwise) apps PROFILE the profile's optional applications from Flathub app add ID... install applications from Flathub (an ID or its end: AndroidStudio, GoLand) app rm ID... remove them The dev profile: dprofile install dev EOF fi } desktop() { command -v Xorg >/dev/null } live() { grep -q 'root=live:' /proc/cmdline 2>/dev/null } profile_names() { local f for f in "$profiles_dir"/*.conf; do [[ -e $f ]] || continue f=${f##*/} echo "${f%.conf}" done } # load NAME: reads the profile into title, desc, packages, services, groups, # subids, shared_root and apps (one "ID NAME" per line) load() { local f=$profiles_dir/$1.conf [[ $1 =~ ^[a-z0-9-]+$ && -r $f ]] || die "немає профілю '$1' (dprofile list)" "no profile '$1' (dprofile list)" title_uk="" title_en="" desc_uk="" desc_en="" server_desc_uk="" server_desc_en="" packages="" desktop_packages="" services="" groups="" subids="" shared_root="" apps="" # shellcheck source=profiles/dev.conf . "$f" title=$(say "$title_uk" "$title_en") desc=$(say "$desc_uk" "$desc_en") if desktop; then packages+=" $desktop_packages" else apps="" [[ -z $server_desc_en ]] || desc=$(say "$server_desc_uk" "$server_desc_en") fi # shellcheck disable=SC2086 packages=$(printf '%s\n' $packages) apps=$(grep . <<<"$apps" || true) } installed() { xbps-query "$1" >/dev/null 2>&1 } # Packages of the loaded profile that are not installed yet missing_packages() { local p while read -r p; do installed "$p" || echo "$p" done <<<"$packages" } app_installed() { command -v flatpak >/dev/null && flatpak info --system "$1" >/dev/null 2>&1 } cmd_list() { local name total missing state for name in $(profile_names); do load "$name" total=$(grep -c . <<<"$packages") missing=$(missing_packages | grep -c . || true) if ((missing == 0)); then state=$(say "встановлено" "installed") elif ((missing < total)); then state=$(say "частково: бракує $missing з $total пакетів" "partly: $missing of $total packages missing") else state=$(say "не встановлено" "not installed") fi pad "$name" 8 echo "$title — $state" echo " $desc" done } cmd_show() { local p id name load "$1" echo "$title: $desc" say "Пакети (* — уже встановлені):" "Packages (* marks the installed ones):" while read -r p; do if installed "$p"; then echo " * $p"; else echo " $p"; fi done <<<"$packages" [[ -z $services ]] || say "Служби: $services" "Services: $services" [[ -z $groups ]] || say "Групи користувача: $groups" "User groups: $groups" if [[ -n $apps ]]; then say "Додатково з Flathub (dprofile app add ID):" "Optional, from Flathub (dprofile app add ID):" while read -r id name; do if app_installed "$id"; then echo " * $(pad "$name" 30)$id"; else echo " $(pad "$name" 30)$id"; fi done <<<"$apps" fi } cmd_apps() { local id name load "$1" [[ -n $apps ]] || { say "У профілю $1 немає додаткових застосунків (або тут немає графічного сеансу)." \ "The $1 profile has no optional applications (or there is no graphical session here)." return } while read -r id name; do if app_installed "$id"; then echo "* $(pad "$name" 30)$id"; else echo " $(pad "$name" 30)$id"; fi done <<<"$apps" } # resolve_app SPEC: the Flathub ID of SPEC, a full ID or its last part in any case resolve_app() { local spec=$1 name id found="" if [[ $spec == *.*.* ]]; then echo "$spec" return fi shopt -s nocasematch for name in $(profile_names); do load "$name" while read -r id _; do [[ -n $id && ${id##*.} == "$spec" ]] && found=$id done <<<"$apps" done shopt -u nocasematch [[ -n $found ]] || die "невідомий застосунок '$spec': повний ID з Flathub або назва з dprofile apps" \ "unknown application '$spec': a full Flathub ID or a name from dprofile apps" echo "$found" } setup_flathub() { flatpak remote-add --system --if-not-exists flathub "$flathub" } # app_add ID...: installs Flathub applications system-wide app_add() { (($# > 0)) || return 0 command -v flatpak >/dev/null || die "немає flatpak: спершу dprofile install dev" "flatpak is missing: dprofile install dev first" setup_flathub say "Встановлюю з Flathub: $*" "Installing from Flathub: $*" # The exports are in the users' XDG_DATA_DIRS (/etc/profile.d/flatpak.sh), # not in root's: without them flatpak warns that the menu won't find the apps XDG_DATA_DIRS=/var/lib/flatpak/exports/share:${XDG_DATA_DIRS:-/usr/local/share:/usr/share} \ flatpak install --system --noninteractive -y flathub "$@" say "Застосунки з'являться в меню після наступного входу." "The applications appear in the menu after the next login." } cmd_app() { local action=${1:-} ids=() spec shift || true desktop || die "застосунки з Flathub — лише для системи з графічним сеансом" \ "Flathub applications are only for a system with a graphical session" (($# > 0)) || die "вкажіть застосунок: dprofile app $action GoLand" "give an application: dprofile app $action GoLand" for spec in "$@"; do ids+=("$(resolve_app "$spec")") done case $action in add | install) need_root app add "${ids[@]}" app_add "${ids[@]}" ;; rm | remove) need_root app rm "${ids[@]}" flatpak uninstall --system --noninteractive -y "${ids[@]}" # The runtimes no other application uses take most of the space flatpak uninstall --system --noninteractive -y --unused ;; *) die "dprofile app add|rm ID..." "dprofile app add|rm ID..." ;; esac } # choose_apps: asks which optional applications to install into "chosen" choose_apps() { local i=0 id name ids=() reply n ok say "Додаткові середовища з Flathub (великі: 1–4 ГБ разом із рантаймами Flatpak):" \ "Optional IDEs from Flathub (large: 1-4 GB with the Flatpak runtimes):" while read -r id name; do ids+=("$id") i=$((i + 1)) if app_installed "$id"; then echo " $i) $(pad "$name" 30)$(say "уже встановлено" "already installed")" else echo " $i) $name" fi done <<<"$apps" while true; do chosen=() ok=1 read -r -p "$(say "Номери через пробіл (Enter — жодного): " "Numbers separated by spaces (Enter for none): ")" reply || return 0 for n in ${reply//,/ }; do if [[ $n =~ ^[0-9]+$ ]] && ((n >= 1 && n <= ${#ids[@]})); then chosen+=("${ids[n - 1]}") else warn "'$n': номер від 1 до ${#ids[@]}" "'$n': a number from 1 to ${#ids[@]}" ok="" fi done [[ -z $ok ]] || return 0 done } # snapper pre/post pair around the installation, when snapshots are set up snapshot_pre() { [[ -e /etc/snapper/configs/root ]] || return 0 snapper -c root create --type pre --print-number --cleanup-algorithm number \ --description "dprofile install $1" } snapshot_post() { [[ -n $1 ]] || return 0 snapper -c root create --type post --pre-number "$1" --cleanup-algorithm number \ --description "dprofile install $2" || true } # add_subids USER: subordinate IDs for rootless containers, after the # highest range already given out add_subids() { local user=$1 file start for file in /etc/subuid /etc/subgid; do touch "$file" grep -q "^$user:" "$file" && continue start=$(awk -F: 'BEGIN { m = 100000 } { if ($2 + $3 > m) m = $2 + $3 } END { print m }' "$file") if [[ $file == /etc/subuid ]]; then usermod --add-subuids "$start-$((start + 65535))" "$user" else usermod --add-subgids "$start-$((start + 65535))" "$user" fi done } # Mount propagation for rootless containers, at every boot and right now make_root_shared() { local f=/etc/runit/core-services/60-das-shared-root.sh [[ -e $f ]] && return cat >"$f" <<'EOF' # Created by dprofile: rootless Podman wants / to be a shared mount mount --make-rshared / EOF mount --make-rshared / } cmd_install() { local name="" yes="" asked="" app_spec="" user=${SUDO_USER:-} missing chosen=() id pre="" sv g joined="" while (($# > 0)); do case $1 in -y | --yes) yes=1 ;; --asked) asked=1 ;; --apps) app_spec=${2:-}; shift ;; --apps=*) app_spec=${1#--apps=} ;; -*) die "невідомий параметр $1" "unknown option $1" ;; *) name=$1 ;; esac shift done [[ -n $name ]] || die "вкажіть профіль: dprofile install dev" "give a profile: dprofile install dev" load "$name" live && die "це live-сесія: спершу встановіть Das OS на диск" "this is a live session: install Das OS to a disk first" [[ $user == root ]] && user="" missing=$(missing_packages | xargs) # The questions come before sudo, so the password is asked for a known plan; # --asked is the run through sudo after them if [[ -z $asked ]]; then echo "$title: $desc" if [[ -n $missing ]]; then say "Буде встановлено: $missing" "To be installed: $missing" else say "Усі пакети вже встановлено." "All packages are installed already." fi if [[ -n $apps && -z $app_spec && -z $yes ]] && interactive; then choose_apps app_spec=$(IFS=,; echo "${chosen[*]}") app_spec=${app_spec:-none} fi if [[ -z $yes ]] && interactive; then confirm "Продовжити?" "Continue?" || exit 1 fi fi need_root install "$name" --asked ${app_spec:+--apps "$app_spec"} chosen=() if [[ -n $apps ]]; then case $app_spec in "" | none) ;; all) mapfile -t chosen < <(awk '{ print $1 }' <<<"$apps") ;; *) for id in ${app_spec//,/ }; do chosen+=("$(resolve_app "$id")") done ;; esac fi if [[ -n $missing ]]; then xbps-install -S # xbps refuses to install anything while an update of itself is pending if xbps-install -un 2>/dev/null | grep -q '^xbps-[0-9]'; then xbps-install -uy xbps fi pre=$(snapshot_pre "$name") || die "не вдалося створити знімок системи, нічого не встановлено" "could not take a system snapshot, nothing was installed" # shellcheck disable=SC2086 if ! xbps-install -y $missing; then snapshot_post "$pre" "$name" die "xbps-install не вдався" "xbps-install failed" fi snapshot_post "$pre" "$name" fi for sv in $services; do if [[ -d /etc/sv/$sv && ! -e /etc/runit/runsvdir/default/$sv ]]; then ln -s "/etc/sv/$sv" /etc/runit/runsvdir/default/ say "Служба $sv увімкнена" "Service $sv enabled" fi done [[ -z $shared_root ]] || make_root_shared if [[ -n $user ]]; then for g in $groups; do if getent group "$g" >/dev/null && ! id -nG "$user" | grep -qw "$g"; then gpasswd -a "$user" "$g" >/dev/null joined+=" $g" fi done [[ -n $subids ]] && add_subids "$user" [[ -z $joined ]] || say "$user тепер у групах:$joined (діє з наступного входу)" \ "$user is now in the groups:$joined (from the next login)" elif [[ -n $groups ]]; then say "Додайте користувачів до груп самі: duser groups КОРИСТУВАЧ +${groups// / +}" \ "Add users to the groups yourself: duser groups USER +${groups// / +}" fi if command -v flatpak >/dev/null && desktop; then setup_flathub app_add "${chosen[@]}" fi say "Профіль $name встановлено." "The $name profile is installed." [[ -z $pre ]] || say "Відкат: у меню завантаження «Das OS snapshots» → знімок №$pre, потім sudo snapper-rollback $pre." \ "To roll back: \"Das OS snapshots\" → snapshot #$pre in the boot menu, then sudo snapper-rollback $pre." } cmd=${1:-list} [[ $# -gt 0 ]] && shift case $cmd in list | ls) cmd_list ;; show | info) [[ $# -gt 0 ]] || die "вкажіть профіль: dprofile show dev" "give a profile: dprofile show dev" cmd_show "$1" ;; # Quiet, for das-welcome: success when every package is installed check) load "${1:-}" [[ -z $(missing_packages) ]] ;; install) cmd_install "$@" ;; apps) [[ $# -gt 0 ]] || die "вкажіть профіль: dprofile apps dev" "give a profile: dprofile apps dev" cmd_apps "$1" ;; app) cmd_app "$@" ;; -h | --help | help) usage ;; *) usage >&2 exit 1 ;; esac