das-tools (Go, GTK 3 via gotk3): - das-update: update window, tray icon when updates are available, --cli mode; xbps-install -Su between a snapper pre/post snapshot pair, privileged part through pkexec, rollback and reboot hints - das-welcome: first-login window with install (live), language, home folder renaming, updates, settings and help das-settings 0.3.0: create home folders with translated names. xdg-user-dirs 0.20 only looks for translations in the first XDG_DATA_DIRS entry with a locale directory (/usr/local/share/locale). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
129 lines
3.6 KiB
Go
129 lines
3.6 KiB
Go
// Package update runs a full system update wrapped in a snapper pre/post
|
|
// snapshot pair, so a broken update can be rolled back from the boot menu.
|
|
package update
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os/exec"
|
|
|
|
"dasos/tools/internal/i18n"
|
|
"dasos/tools/internal/snapper"
|
|
"dasos/tools/internal/xbps"
|
|
)
|
|
|
|
// Description marks the snapshots in "snapper list".
|
|
const Description = "das-update"
|
|
|
|
// Reporter receives the progress of Apply.
|
|
type Reporter interface {
|
|
Step(title string)
|
|
// Output receives the output of the commands Apply runs.
|
|
Output() io.Writer
|
|
// Snapshot is called once the "pre" snapshot exists.
|
|
Snapshot(n int)
|
|
}
|
|
|
|
// Result describes a finished update.
|
|
type Result struct {
|
|
Updates []xbps.Update
|
|
Snapshot int // number of the "pre" snapshot, 0 without snapshots
|
|
Reboot bool // the kernel or core libraries changed
|
|
}
|
|
|
|
// Updater holds the system operations, replaced in tests.
|
|
type Updater struct {
|
|
Run func(ctx context.Context, out io.Writer, name string, args ...string) error
|
|
Pending func(ctx context.Context) ([]xbps.Update, error)
|
|
Snapshots func() bool
|
|
Pre func(ctx context.Context, description string) (int, error)
|
|
Post func(ctx context.Context, pre int, description string) error
|
|
}
|
|
|
|
// System returns the Updater for the running system. It needs root.
|
|
func System() *Updater {
|
|
return &Updater{
|
|
Run: run,
|
|
Pending: pending,
|
|
Snapshots: snapper.Available,
|
|
Pre: snapper.Pre,
|
|
Post: snapper.Post,
|
|
}
|
|
}
|
|
|
|
func run(ctx context.Context, out io.Writer, name string, args ...string) error {
|
|
cmd := exec.CommandContext(ctx, name, args...)
|
|
cmd.Stdout = out
|
|
cmd.Stderr = out
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("%s: %w", name, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// pending lists updates against the index synced by the first step.
|
|
func pending(ctx context.Context) ([]xbps.Update, error) {
|
|
out, err := exec.CommandContext(ctx, "xbps-install", "-un").CombinedOutput()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("xbps-install -un: %w: %s", err, out)
|
|
}
|
|
return xbps.ParseDryRun(string(out)), nil
|
|
}
|
|
|
|
// Apply updates the whole system. Without pending updates it does nothing
|
|
// and returns an empty Result. When snapshots are set up, a failure to take
|
|
// the "pre" snapshot stops the update: updating without a way back is
|
|
// exactly what das-update exists to prevent.
|
|
func (u *Updater) Apply(ctx context.Context, r Reporter) (Result, error) {
|
|
var res Result
|
|
|
|
r.Step(i18n.T("update.step.sync"))
|
|
if err := u.Run(ctx, r.Output(), "xbps-install", "-S"); err != nil {
|
|
return res, err
|
|
}
|
|
updates, err := u.Pending(ctx)
|
|
if err != nil {
|
|
// An outdated xbps refuses to plan the update; it is updated first anyway.
|
|
updates = []xbps.Update{{Name: "xbps", Action: "update"}}
|
|
}
|
|
if len(updates) == 0 {
|
|
return res, nil
|
|
}
|
|
res.Updates = updates
|
|
res.Reboot = xbps.NeedsReboot(updates)
|
|
|
|
if u.Snapshots() {
|
|
r.Step(i18n.T("update.step.pre"))
|
|
n, err := u.Pre(ctx, Description)
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
res.Snapshot = n
|
|
r.Snapshot(n)
|
|
} else {
|
|
r.Step(i18n.T("update.nosnapshots"))
|
|
}
|
|
|
|
err = u.install(ctx, r, updates)
|
|
if res.Snapshot > 0 {
|
|
// Taken after a failure too: the pair shows what the update changed.
|
|
r.Step(i18n.T("update.step.post"))
|
|
if perr := u.Post(ctx, res.Snapshot, Description); perr != nil && err == nil {
|
|
err = perr
|
|
}
|
|
}
|
|
return res, err
|
|
}
|
|
|
|
func (u *Updater) install(ctx context.Context, r Reporter, updates []xbps.Update) error {
|
|
if xbps.SelfUpdate(updates) {
|
|
r.Step(i18n.T("update.step.xbps"))
|
|
if err := u.Run(ctx, r.Output(), "xbps-install", "-yu", "xbps"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
r.Step(i18n.T("update.step.install"))
|
|
return u.Run(ctx, r.Output(), "xbps-install", "-yu")
|
|
}
|