Stage 6: server image with a console installer (0.4.0)

- server/ profile and build-iso.sh [desktop|server]; make iso-server,
  make qemu-server, run-qemu.sh --serial; login on tty and ttyS0
- das-server: SSH, chrony, NetworkManager/dhcpcd, Docker with Compose,
  nftables (closed except SSH, own table), fail2ban reading socklog,
  sshd without root passwords, Docker log rotation, tools
- das-server-install: dialog installer in Ukrainian and English, or
  unattended with --config; GPT + Btrfs subvolumes, GRUB for UEFI and BIOS,
  serial console, SSH key with optional keys-only logins
- das-install-common: mount-source and postinstall shared with Calamares
- dsv: runit service manager (list, enable, disable, start, stop,
  restart, status, logs) with bash completion, also on the desktop
- das-update split into a subpackage without GTK; the window and tray
  icon move to /usr/libexec/das/das-update-gui
- das-snapper-setup enables dbus, which snapper needs
- TODO: ISO size review with measured numbers and options

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
das
2026-10-19 21:28:26 +03:00
co-authored by Claude Opus 5.5
parent 5349a028c8
commit eba4516998
42 changed files with 1454 additions and 114 deletions
@@ -0,0 +1,27 @@
// Command das-update-gui is the desktop part of das-update, installed as
// /usr/libexec/das/das-update-gui by das-tools. das-update starts it when a
// display is available, so servers get das-update without GTK.
//
// das-update-gui window that lists and installs updates
// das-update-gui --tray background check, shows a tray icon when updates are available
package main
import (
"flag"
"os"
"dasos/tools/internal/i18n"
)
func main() {
tray := flag.Bool("tray", false, "check for updates in the background")
lang := flag.String("lang", "", "message language, e.g. uk_UA.UTF-8")
flag.Parse()
if *lang != "" {
i18n.SetLang(*lang)
}
if *tray {
os.Exit(runTray())
}
os.Exit(runWindow())
}
@@ -16,6 +16,7 @@ import (
"dasos/tools/internal/i18n"
"dasos/tools/internal/system"
"dasos/tools/internal/update"
"dasos/tools/internal/xbps"
)
@@ -265,20 +266,20 @@ func (w *window) startInstall() {
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, markStep):
line = "\n==> " + strings.TrimPrefix(line, markStep)
case strings.HasPrefix(line, markSnapshot):
n, _ := strconv.Atoi(strings.TrimPrefix(line, markSnapshot))
case strings.HasPrefix(line, update.MarkStep):
line = "\n==> " + strings.TrimPrefix(line, update.MarkStep)
case strings.HasPrefix(line, update.MarkSnapshot):
n, _ := strconv.Atoi(strings.TrimPrefix(line, update.MarkSnapshot))
glib.IdleAdd(func() { w.snapshot = n })
line = i18n.T("update.snapshot", n)
case line == markReboot:
case line == update.MarkReboot:
reboot = true
continue
case line == markUpToDate:
case line == update.MarkUpToDate:
upToDate = true
continue
case strings.HasPrefix(line, markError):
failure = strings.TrimPrefix(line, markError)
case strings.HasPrefix(line, update.MarkError):
failure = strings.TrimPrefix(line, update.MarkError)
}
glib.IdleAdd(func() { w.appendLog(line + "\n") })
}
@@ -1,10 +1,13 @@
// Command das-update updates Das OS with a snapper snapshot taken before
// the update, so a broken update can be rolled back from the boot menu.
//
// das-update window that lists and installs updates
// das-update the update window on a desktop, otherwise the same as --cli
// das-update --tray background check, shows a tray icon when updates are available
// das-update --cli the same in a terminal (servers, ssh); -y skips the question
// das-update --cli update in a terminal (servers, ssh); -y skips the question
// das-update --apply the privileged part, started by the window through pkexec
//
// The window and the tray icon are a separate program from das-tools
// (guiPath), so this one has no GTK dependency.
package main
import (
@@ -42,27 +45,42 @@ func main() {
case *cli:
os.Exit(runCLI(*yes))
case *tray:
os.Exit(runTray())
// Nothing to show without the desktop part
if haveGUI() {
execGUI()
}
default:
os.Exit(runWindow())
if haveGUI() && haveDisplay() {
execGUI()
}
os.Exit(runCLI(*yes))
}
}
// Machine-readable lines that --apply prints for the window, among the
// plain output of xbps.
const (
markStep = "@@step "
markSnapshot = "@@snapshot "
markReboot = "@@reboot"
markUpToDate = "@@uptodate"
markError = "@@error "
)
// guiPath is the window and tray icon program from das-tools.
const guiPath = "/usr/libexec/das/das-update-gui"
func haveGUI() bool {
_, err := os.Stat(guiPath)
return err == nil
}
func haveDisplay() bool {
return os.Getenv("DISPLAY") != "" || os.Getenv("WAYLAND_DISPLAY") != ""
}
// execGUI replaces this process with the graphical part, passing the
// arguments on. It returns only when that fails.
func execGUI() {
err := syscall.Exec(guiPath, append([]string{guiPath}, os.Args[1:]...), os.Environ())
fmt.Fprintln(os.Stderr, "das-update:", err)
}
type markReporter struct{ w io.Writer }
func (r markReporter) Step(title string) { fmt.Fprintln(r.w, markStep+title) }
func (r markReporter) Step(title string) { fmt.Fprintln(r.w, update.MarkStep+title) }
func (r markReporter) Output() io.Writer { return r.w }
func (r markReporter) Snapshot(n int) { fmt.Fprintf(r.w, "%s%d\n", markSnapshot, n) }
func (r markReporter) Snapshot(n int) { fmt.Fprintf(r.w, "%s%d\n", update.MarkSnapshot, n) }
func runApply() int {
if os.Geteuid() != 0 {
@@ -74,13 +92,13 @@ func runApply() int {
r := markReporter{os.Stdout}
res, err := update.System().Apply(context.Background(), r)
if err != nil {
fmt.Println(markError + err.Error())
fmt.Println(update.MarkError + err.Error())
return 1
}
if len(res.Updates) == 0 {
fmt.Println(markUpToDate)
fmt.Println(update.MarkUpToDate)
} else if res.Reboot {
fmt.Println(markReboot)
fmt.Println(update.MarkReboot)
}
return 0
}
@@ -0,0 +1,11 @@
package update
// Machine-readable lines that "das-update --apply" prints among the plain
// output of xbps, for the window that runs it through pkexec.
const (
MarkStep = "@@step "
MarkSnapshot = "@@snapshot "
MarkReboot = "@@reboot"
MarkUpToDate = "@@uptodate"
MarkError = "@@error "
)