dprofile (das-helpers 0.2.0) installs profiles listed in /usr/share/das/profiles: packages from the Void repository between a snapper pre/post pair, their services and groups, subordinate IDs and a shared root mount for rootless Podman, and optional IDEs from Flathub. The installed system can't see the Das OS repository yet, so a das-dev metapackage could not be installed there; Void packages always can. The dev profile: Go, Python, Node.js, Git, Docker, Podman and command-line tools everywhere, VS Code (code-oss), Meld and Flatpak on a desktop, with Android Studio and JetBrains IDEs on demand. Questions come before sudo. das-welcome (das-tools 0.4.0) gets a developer profile row that runs dprofile in a terminal; das-server-install mentions it at the end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
417 lines
14 KiB
Bash
Executable File
417 lines
14 KiB
Bash
Executable File
#!/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
|