Stage 8: developer profile with dprofile (0.6.0)
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>
This commit is contained in:
Executable
+416
@@ -0,0 +1,416 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user