Stage 5: das-update and das-welcome (0.3.0)
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>
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
// Package xbps finds pending system updates with xbps.
|
||||
package xbps
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Update is one package that a full system update would change.
|
||||
type Update struct {
|
||||
Name string
|
||||
OldVersion string // empty for a new dependency
|
||||
NewVersion string
|
||||
Action string // "update", "install", "remove" ...
|
||||
}
|
||||
|
||||
// Check lists pending updates without root: -M syncs the repository index
|
||||
// into memory only, -n makes it a dry run.
|
||||
func Check(ctx context.Context) ([]Update, error) {
|
||||
out, err := exec.CommandContext(ctx, "xbps-install", "-SMun").CombinedOutput()
|
||||
if err != nil {
|
||||
// xbps refuses to plan anything while xbps itself is outdated
|
||||
if strings.Contains(string(out), "must be updated") {
|
||||
return []Update{{Name: "xbps", Action: "update"}}, nil
|
||||
}
|
||||
if msg := strings.TrimSpace(string(out)); msg != "" {
|
||||
return nil, fmt.Errorf("xbps-install: %w: %s", err, msg)
|
||||
}
|
||||
return nil, fmt.Errorf("xbps-install: %w", err)
|
||||
}
|
||||
updates := ParseDryRun(string(out))
|
||||
if len(updates) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
listed, err := exec.CommandContext(ctx, "xbps-query", "-l").Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("xbps-query: %w", err)
|
||||
}
|
||||
installed := ParseInstalled(string(listed))
|
||||
for i := range updates {
|
||||
updates[i].OldVersion = installed[updates[i].Name]
|
||||
}
|
||||
return updates, nil
|
||||
}
|
||||
|
||||
// ParseDryRun parses "xbps-install -un" output: one transaction entry per
|
||||
// line, "<pkgver> <action> <arch> <repository> [sizes]". Other lines are skipped.
|
||||
func ParseDryRun(out string) []Update {
|
||||
var updates []Update
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
f := strings.Fields(line)
|
||||
if len(f) < 3 {
|
||||
continue
|
||||
}
|
||||
name, version, ok := SplitPkgver(f[0])
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch f[1] {
|
||||
case "update", "install", "remove", "reinstall", "downgrade":
|
||||
updates = append(updates, Update{Name: name, NewVersion: version, Action: f[1]})
|
||||
}
|
||||
}
|
||||
sort.Slice(updates, func(i, j int) bool { return updates[i].Name < updates[j].Name })
|
||||
return updates
|
||||
}
|
||||
|
||||
// ParseInstalled parses "xbps-query -l" ("ii <pkgver> <description>") into
|
||||
// a name → version map.
|
||||
func ParseInstalled(out string) map[string]string {
|
||||
m := make(map[string]string)
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
f := strings.Fields(line)
|
||||
if len(f) < 2 {
|
||||
continue
|
||||
}
|
||||
if name, version, ok := SplitPkgver(f[1]); ok {
|
||||
m[name] = version
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// SplitPkgver splits "name-version_revision" at the last dash. The version
|
||||
// part always contains "_revision", which tells pkgvers from other words.
|
||||
func SplitPkgver(pkgver string) (name, version string, ok bool) {
|
||||
i := strings.LastIndexByte(pkgver, '-')
|
||||
if i <= 0 || i == len(pkgver)-1 || !strings.Contains(pkgver[i+1:], "_") {
|
||||
return "", "", false
|
||||
}
|
||||
return pkgver[:i], pkgver[i+1:], true
|
||||
}
|
||||
|
||||
// NeedsReboot reports whether updating these packages replaces something
|
||||
// that a running system keeps loaded: the kernel, the C library or the
|
||||
// init system.
|
||||
func NeedsReboot(updates []Update) bool {
|
||||
for _, u := range updates {
|
||||
switch {
|
||||
case u.Name == "glibc", u.Name == "runit", u.Name == "runit-void", u.Name == "dbus",
|
||||
u.Name == "elogind", u.Name == "eudev", u.Name == "linux-firmware":
|
||||
return true
|
||||
case strings.HasPrefix(u.Name, "linux") && strings.Trim(u.Name[len("linux"):], "0123456789.") == "":
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SelfUpdate reports whether xbps itself is in the list. xbps must be
|
||||
// updated on its own before the rest of the system.
|
||||
func SelfUpdate(updates []Update) bool {
|
||||
for _, u := range updates {
|
||||
if u.Name == "xbps" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package xbps
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseDryRun(t *testing.T) {
|
||||
out := `[*] Updating repository ` + "`https://repo-default.voidlinux.org/current/x86_64-repodata'" + ` ...
|
||||
firefox-144.0_1 update x86_64 https://repo-default.voidlinux.org/current 245MB 70MB
|
||||
libfoo-2.1_3 install x86_64 https://repo-default.voidlinux.org/current 1024KB 300KB
|
||||
linux6.12-6.12.50_1 update x86_64 https://repo-default.voidlinux.org/current
|
||||
gtk+3-3.24.50_1 update x86_64 https://repo-default.voidlinux.org/current
|
||||
oldpkg-1.0_1 remove x86_64 https://repo-default.voidlinux.org/current
|
||||
bash-5.3_2 configure x86_64 https://repo-default.voidlinux.org/current
|
||||
`
|
||||
got := ParseDryRun(out)
|
||||
want := []Update{
|
||||
{Name: "firefox", NewVersion: "144.0_1", Action: "update"},
|
||||
{Name: "gtk+3", NewVersion: "3.24.50_1", Action: "update"},
|
||||
{Name: "libfoo", NewVersion: "2.1_3", Action: "install"},
|
||||
{Name: "linux6.12", NewVersion: "6.12.50_1", Action: "update"},
|
||||
{Name: "oldpkg", NewVersion: "1.0_1", Action: "remove"},
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("ParseDryRun:\n got %+v\nwant %+v", got, want)
|
||||
}
|
||||
if got := ParseDryRun(""); len(got) != 0 {
|
||||
t.Errorf("ParseDryRun(empty) = %+v, want none", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInstalled(t *testing.T) {
|
||||
out := "ii acl-2.4.0_1 Access Control List filesystem support\n" +
|
||||
"ii base-container-0.3_3 Void Linux base system meta\n" +
|
||||
"\n"
|
||||
want := map[string]string{"acl": "2.4.0_1", "base-container": "0.3_3"}
|
||||
if got := ParseInstalled(out); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("ParseInstalled = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitPkgver(t *testing.T) {
|
||||
tests := []struct {
|
||||
in, name, version string
|
||||
ok bool
|
||||
}{
|
||||
{"xbps-0.60.5_1", "xbps", "0.60.5_1", true},
|
||||
{"font-jetbrains-mono-2.304_1", "font-jetbrains-mono", "2.304_1", true},
|
||||
{"[*]", "", "", false},
|
||||
{"Updating", "", "", false},
|
||||
{"name-", "", "", false},
|
||||
{"no-revision-1.0", "", "", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
name, version, ok := SplitPkgver(tt.in)
|
||||
if name != tt.name || version != tt.version || ok != tt.ok {
|
||||
t.Errorf("SplitPkgver(%q) = %q, %q, %v; want %q, %q, %v", tt.in, name, version, ok, tt.name, tt.version, tt.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNeedsReboot(t *testing.T) {
|
||||
tests := []struct {
|
||||
names []string
|
||||
want bool
|
||||
}{
|
||||
{[]string{"firefox", "mousepad"}, false},
|
||||
{[]string{"linux6.12"}, true},
|
||||
{[]string{"linux"}, true},
|
||||
{[]string{"glibc"}, true},
|
||||
{[]string{"linux-headers", "linux-firmware-network"}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
var updates []Update
|
||||
for _, n := range tt.names {
|
||||
updates = append(updates, Update{Name: n})
|
||||
}
|
||||
if got := NeedsReboot(updates); got != tt.want {
|
||||
t.Errorf("NeedsReboot(%v) = %v, want %v", tt.names, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user