mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-26 04:57:47 -04:00
add portage backend (#3676)
* add portage backend * portage: add excludes
This commit is contained in:
1 parent
30c7171836
commit
375aa2f492
8 files changed
+495
-10
No files matched your search
@@ -251,16 +251,12 @@ func filterUpdateTargets(pkgs []sysupdate.Package) []sysupdate.Package {
|
||||
if !sysUpdateNoAUR && len(sysUpdateIgnore) == 0 {
|
||||
return pkgs
|
||||
}
|
||||
ignored := make(map[string]bool, len(sysUpdateIgnore))
|
||||
for _, name := range sysUpdateIgnore {
|
||||
ignored[name] = true
|
||||
}
|
||||
out := pkgs[:0]
|
||||
for _, p := range pkgs {
|
||||
if sysUpdateNoAUR && p.Repo == sysupdate.RepoAUR {
|
||||
continue
|
||||
}
|
||||
if ignored[p.Name] {
|
||||
if sysupdate.IsPackageIgnored(p.Name, sysUpdateIgnore) {
|
||||
continue
|
||||
}
|
||||
out = append(out, p)
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
package sysupdate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/notify"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterSystemBackend(func() Backend { return &portageBackend{} })
|
||||
}
|
||||
|
||||
// [ebuild U ] sys-apps/util-linux-2.40.4::gentoo [2.40.2::gentoo] USE="..."
|
||||
var (
|
||||
portageUpdateLine = regexp.MustCompile(`^\s*\[(ebuild|binary)\b[^\]]*\]\s+([^\s/]+)/([^\s\[\]]+?)::([^\s\[\]]+)(?:\s+\[\s*([^\s\]]+?)(?:::[^\s\]]+)?\])?`)
|
||||
ansiRegexp = regexp.MustCompile(`\x1B\[[0-9;?]*[ -/]*[@-~]`)
|
||||
)
|
||||
|
||||
func stripANSI(s string) string {
|
||||
return ansiRegexp.ReplaceAllString(s, "")
|
||||
}
|
||||
|
||||
type portageBackend struct{}
|
||||
|
||||
func (portageBackend) ID() string { return "portage" }
|
||||
func (portageBackend) DisplayName() string { return "Portage" }
|
||||
func (portageBackend) Repo() RepoKind { return RepoSystem }
|
||||
func (portageBackend) NeedsAuth() bool { return true }
|
||||
func (portageBackend) RunsInTerminal() bool { return false }
|
||||
func (portageBackend) IsAvailable(_ context.Context) bool { return commandExists("emerge") }
|
||||
|
||||
func (b portageBackend) CheckUpdates(ctx context.Context) ([]Package, error) {
|
||||
out, err := capturePortageUpdates(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parsePortageUpdates(out, b.ID()), nil
|
||||
}
|
||||
|
||||
func (b portageBackend) Upgrade(ctx context.Context, opts UpgradeOptions, onLine func(string)) error {
|
||||
err := b.upgrade(ctx, opts, onLine)
|
||||
if err != nil && isPortageAuthCanceled(err) {
|
||||
return err
|
||||
}
|
||||
if err != nil && !opts.DryRun && ctx.Err() == nil {
|
||||
notifyPortageFailure()
|
||||
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Notify the user that portage failed so they can check the logs and fix the issue manually
|
||||
func notifyPortageFailure() {
|
||||
if _, err := notify.Send(notify.Notification{
|
||||
Summary: "Portage update failed",
|
||||
Body: "Open Settings > Software Updates to read the error log.",
|
||||
Timeout: 10000,
|
||||
}); err != nil {
|
||||
log.Warnf("[sysupdate] portage failure notification: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func isPortageAuthCanceled(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
msg := strings.ToLower(err.Error())
|
||||
return strings.Contains(msg, "exit status 126")
|
||||
}
|
||||
|
||||
func (b portageBackend) upgrade(ctx context.Context, opts UpgradeOptions, onLine func(string)) error {
|
||||
if opts.DryRun {
|
||||
return Run(ctx, []string{"emerge", "--pretend", "--update", "--newuse", "--deep", "@world"}, RunOptions{OnLine: onLine})
|
||||
}
|
||||
if !BackendHasTargets(b, opts.Targets, opts.IncludeAUR, opts.IncludeFlatpak) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(opts.Targets) > 0 && hasPortageTarget(opts.Targets) && !portageIsExcluded(opts) {
|
||||
if err := runPortageFirst(ctx, opts, onLine); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ansiWrap := func(s string) {
|
||||
if onLine != nil {
|
||||
onLine(stripANSI(s))
|
||||
}
|
||||
}
|
||||
return Run(ctx, portageUpgradeArgv(opts), RunOptions{OnLine: ansiWrap, AttachStdio: opts.AttachStdio})
|
||||
}
|
||||
|
||||
// emerge upgrades all of @world, so ignores are the only way to hold a package back
|
||||
func portageUpgradeArgv(opts UpgradeOptions) []string {
|
||||
argv := []string{"emerge", "--update", "--newuse", "--deep", "--quiet"}
|
||||
for _, atom := range shellSafeNames(opts.Ignored) {
|
||||
argv = append(argv, "--exclude", atom)
|
||||
}
|
||||
argv = append(argv, "@world")
|
||||
return privilegedArgv(opts, argv...)
|
||||
}
|
||||
|
||||
// Portage can report names without slots, so a sloted ignore only matches slotless packages when it is slot 0.
|
||||
func PackageMatchesIgnore(pkgName, ignored string) bool {
|
||||
pkgBase, pkgSlot, _ := strings.Cut(pkgName, ":")
|
||||
ignBase, ignSlot, _ := strings.Cut(ignored, ":")
|
||||
if pkgSlot != "" && ignSlot != "" {
|
||||
return strings.EqualFold(pkgBase, ignBase) && pkgSlot == ignSlot
|
||||
}
|
||||
if ignSlot != "" && pkgSlot == "" {
|
||||
return strings.EqualFold(pkgBase, ignBase) && ignSlot == "0"
|
||||
}
|
||||
return strings.EqualFold(pkgBase, ignBase)
|
||||
}
|
||||
|
||||
// IsPackageIgnored reports whether any ignore entry holds pkgName back.
|
||||
func IsPackageIgnored(pkgName string, ignored []string) bool {
|
||||
for _, ign := range ignored {
|
||||
if PackageMatchesIgnore(pkgName, ign) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// An ignored portage means the user holds it back, so the pre-update step must skip it too
|
||||
func portageIsExcluded(opts UpgradeOptions) bool {
|
||||
for _, ign := range opts.Ignored {
|
||||
if IsPackageIgnored("sys-apps/portage", []string{ign}) {
|
||||
return true
|
||||
}
|
||||
base, _, _ := strings.Cut(ign, ":")
|
||||
if isPortagePackage(base) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isPortagePackage(name string) bool {
|
||||
l := strings.ToLower(name)
|
||||
return l == "sys-apps/portage" || l == "portage" || strings.HasSuffix(l, "/portage")
|
||||
}
|
||||
|
||||
func hasPortageTarget(targets []Package) bool {
|
||||
for _, p := range targets {
|
||||
if p.Backend == "portage" && isPortagePackage(p.Name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Before running any other system updates, portage should always be updated first
|
||||
func runPortageFirst(ctx context.Context, opts UpgradeOptions, onLine func(string)) error {
|
||||
argv := []string{"emerge", "--oneshot", "sys-apps/portage"}
|
||||
return Run(ctx, privilegedArgv(opts, argv...), RunOptions{OnLine: onLine, AttachStdio: opts.AttachStdio})
|
||||
}
|
||||
|
||||
// Portage needs to sync the repos first before we can check for any available updates
|
||||
// This requires root, there is no good way around this
|
||||
// even if this causes the user to suddenly get pkexec prompts if they enable background updates
|
||||
func portageSync(ctx context.Context) (string, error) {
|
||||
argv := privilegedArgv(UpgradeOptions{}, "emerge", "--sync")
|
||||
var out strings.Builder
|
||||
cmd := exec.CommandContext(ctx, argv[0], argv[1:]...)
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &out
|
||||
err := cmd.Run()
|
||||
if err != nil && isPortageAuthCanceled(err) {
|
||||
return out.String(), errors.New("authentication request was dismissed or canceled")
|
||||
}
|
||||
if err != nil {
|
||||
return out.String(), portageRunError(err, out.String())
|
||||
}
|
||||
return out.String(), nil
|
||||
}
|
||||
|
||||
func portageRunError(err error, output string) error {
|
||||
if detail := strings.TrimSpace(output); detail != "" {
|
||||
return fmt.Errorf("%w: %s", err, detail)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func capturePortageUpdates(ctx context.Context) (string, error) {
|
||||
if _, err := portageSync(ctx); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "emerge", "--pretend", "--verbose", "--update", "--newuse", "--deep", "--color=n", "@world")
|
||||
cmd.Env = append(cmd.Environ(), "TERM=dumb", "LC_ALL=C")
|
||||
var out, errOut strings.Builder
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &errOut
|
||||
// Any non-zero exit means @world does not resolve, and a broken world prints no update list, so it must not read as up to date.
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", portageRunError(err, out.String()+errOut.String())
|
||||
}
|
||||
return out.String(), nil
|
||||
}
|
||||
|
||||
func splitPkgVersion(pkgVer string) (pkg, ver string) {
|
||||
for i := 0; i < len(pkgVer)-1; i++ {
|
||||
if pkgVer[i] == '-' && pkgVer[i+1] >= '0' && pkgVer[i+1] <= '9' {
|
||||
return pkgVer[:i], pkgVer[i+1:]
|
||||
}
|
||||
}
|
||||
return pkgVer, pkgVer
|
||||
}
|
||||
|
||||
func extractPkgAndVersion(pkgVer string) (pkg, ver string) {
|
||||
pkg, ver = splitPkgVersion(pkgVer)
|
||||
if pkg == pkgVer {
|
||||
return "", ver
|
||||
}
|
||||
return pkg, ver
|
||||
}
|
||||
|
||||
func parsePortageUpdates(text, backendID string) []Package {
|
||||
if text == "" {
|
||||
return nil
|
||||
}
|
||||
var pkgs []Package
|
||||
for line := range strings.SplitSeq(text, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
line = stripANSI(line)
|
||||
m := portageUpdateLine.FindStringSubmatch(line)
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
category := m[2]
|
||||
pkgBase, newVer := splitPkgVersion(m[3])
|
||||
_, oldVer := extractPkgAndVersion(m[5])
|
||||
pkgs = append(pkgs, Package{
|
||||
Name: category + "/" + pkgBase,
|
||||
Repo: portageRepoLabel(m[1], m[4]),
|
||||
Backend: backendID,
|
||||
FromVersion: oldVer,
|
||||
ToVersion: newVer,
|
||||
})
|
||||
}
|
||||
return pkgs
|
||||
}
|
||||
|
||||
// The GUI shows repo and build type in one slot, since Package has no field for it.
|
||||
func portageRepoLabel(build, repo string) RepoKind {
|
||||
if repo == "" {
|
||||
repo = "gentoo"
|
||||
}
|
||||
return RepoKind(repo + " (" + build + ")")
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package sysupdate
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPortagePretendFailureIsNotAnEmptyUpdateList(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("PATH", dir)
|
||||
writeUpdateExecutable(t, dir, "pkexec", `exec "$@"`)
|
||||
writeUpdateExecutable(t, dir, "emerge", `case "$1" in
|
||||
--sync) exit 0 ;;
|
||||
--pretend) echo '!!! The following packages are blocked by another package:' >&2; exit 1 ;;
|
||||
esac`)
|
||||
|
||||
out, err := capturePortageUpdates(t.Context())
|
||||
if err == nil {
|
||||
t.Fatalf("capturePortageUpdates() = %q, want error", out)
|
||||
}
|
||||
if out != "" {
|
||||
t.Errorf("capturePortageUpdates() output = %q, want empty on failure", out)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "blocked by another package") {
|
||||
t.Errorf("error lost emerge stderr: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePortageUpdates(t *testing.T) {
|
||||
input := `These are the packages that would be merged, in order:
|
||||
|
||||
Calculating dependencies ... done!
|
||||
[ebuild U ] app-text/ansifilter-2.23::gentoo [2.22::gentoo] USE="gui -verify-sig" 0 KiB
|
||||
[ebuild U ~] sys-apps/kmscon-10.0.4::gentoo [10.0.3::gentoo] USE="drm fbdev gles2 pango -debug" 0 KiB
|
||||
[ebuild UD~] gui-wm/umbriel-0.0.0_pre20260924::guru [0.0.0_pre20261003::guru] USE="X%* screencast -jemalloc" 0 KiB
|
||||
[binary gU ] mail-client/thunderbird-153.4.0-1:0/esr::gentoo [153.3.0:0/esr::gentoo] USE="X clang pulseaudio" 0 KiB
|
||||
[binary rRg ] media-video/mpv-0.41.0-r2-21:0/2::gentoo USE="X alsa cdda cli" 0 KiB
|
||||
[ebuild U ] www-client/librewolf-bin-157.0_p1::librewolf [156.0.1_p1::librewolf] USE="wayland (-selinux)" 0 KiB
|
||||
[ebuild U ] dev-lang/rust-1.83.0:1.83::gentoo [1.82.0:1.82::gentoo] USE="lto rustfmt -clippy" 0 KiB
|
||||
[ebuild U ] sys-libs/glibc-2.41-r3:2.2::gentoo [2.41-r2:2.2::gentoo] USE="multiarch -audit" 0 KiB
|
||||
|
||||
Total: 8 packages (8 upgrades), Size of downloads: 0 KiB
|
||||
`
|
||||
|
||||
want := []Package{
|
||||
{Name: "app-text/ansifilter", Repo: "gentoo (ebuild)", Backend: "portage", FromVersion: "2.22", ToVersion: "2.23"},
|
||||
{Name: "sys-apps/kmscon", Repo: "gentoo (ebuild)", Backend: "portage", FromVersion: "10.0.3", ToVersion: "10.0.4"},
|
||||
{Name: "gui-wm/umbriel", Repo: "guru (ebuild)", Backend: "portage", FromVersion: "0.0.0_pre20261003", ToVersion: "0.0.0_pre20260924"},
|
||||
{Name: "mail-client/thunderbird", Repo: "gentoo (binary)", Backend: "portage", FromVersion: "153.3.0:0/esr", ToVersion: "153.4.0-1:0/esr"},
|
||||
{Name: "media-video/mpv", Repo: "gentoo (binary)", Backend: "portage", ToVersion: "0.41.0-r2-21:0/2"},
|
||||
{Name: "www-client/librewolf-bin", Repo: "librewolf (ebuild)", Backend: "portage", FromVersion: "156.0.1_p1", ToVersion: "157.0_p1"},
|
||||
{Name: "dev-lang/rust", Repo: "gentoo (ebuild)", Backend: "portage", FromVersion: "1.82.0:1.82", ToVersion: "1.83.0:1.83"},
|
||||
{Name: "sys-libs/glibc", Repo: "gentoo (ebuild)", Backend: "portage", FromVersion: "2.41-r2:2.2", ToVersion: "2.41-r3:2.2"},
|
||||
}
|
||||
|
||||
got := parsePortageUpdates(input, "portage")
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("parsePortageUpdates() =\n%#v\nwant\n%#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPortageRepoLabelDefaultsToGentoo(t *testing.T) {
|
||||
if got := portageRepoLabel("ebuild", ""); got != RepoKind("gentoo (ebuild)") {
|
||||
t.Errorf("portageRepoLabel(no repo) = %q, want %q", got, "gentoo (ebuild)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPortageIsExcluded(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ignored []string
|
||||
excluded bool
|
||||
}{
|
||||
{name: "no ignores", ignored: nil},
|
||||
{name: "portage ignored", ignored: []string{"sys-apps/portage"}, excluded: true},
|
||||
{name: "portage slot ignored", ignored: []string{"sys-apps/portage:0"}, excluded: true},
|
||||
{name: "portage nonzero slot ignored", ignored: []string{"sys-apps/portage:2"}, excluded: true},
|
||||
{name: "other package ignored", ignored: []string{"mail-client/thunderbird", "bad;name"}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := portageIsExcluded(UpgradeOptions{Ignored: tt.ignored}); got != tt.excluded {
|
||||
t.Fatalf("portageIsExcluded(%v) = %v, want %v", tt.ignored, got, tt.excluded)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackageMatchesIgnore(t *testing.T) {
|
||||
tests := []struct {
|
||||
pkg string
|
||||
ignored string
|
||||
want bool
|
||||
}{
|
||||
{pkg: "gui-wm/gamescope", ignored: "gui-wm/gamescope", want: true},
|
||||
{pkg: "gui-wm/gamescope", ignored: "gui-wm/sway"},
|
||||
{pkg: "app-misc/fastfetch:0", ignored: "app-misc/fastfetch", want: true},
|
||||
{pkg: "app-misc/fastfetch:0", ignored: "app-misc/fastfetch:0", want: true},
|
||||
{pkg: "app-misc/fastfetch:2", ignored: "app-misc/fastfetch:0"},
|
||||
{pkg: "app-misc/fastfetch:2", ignored: "app-misc/fastfetch:2", want: true},
|
||||
{pkg: "app-misc/fastfetch:5", ignored: "app-misc/fastfetch:5", want: true},
|
||||
{pkg: "app-misc/fastfetch:2", ignored: "app-misc/fastfetch:0"},
|
||||
{pkg: "app-misc/fastfetch:5", ignored: "app-misc/fastfetch:0"},
|
||||
{pkg: "app-misc/fastfetch:2", ignored: "app-misc/fastfetch:5"},
|
||||
{pkg: "app-misc/fastfetch", ignored: "app-misc/fastfetch:0", want: true},
|
||||
{pkg: "app-misc/fastfetch", ignored: "app-misc/fastfetch:2"},
|
||||
{pkg: "app-misc/fastfetch", ignored: "app-misc/fastfetch:5"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := PackageMatchesIgnore(tt.pkg, tt.ignored); got != tt.want {
|
||||
t.Errorf("PackageMatchesIgnore(%q, %q) = %v, want %v", tt.pkg, tt.ignored, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package sysupdate
|
||||
|
||||
import "regexp"
|
||||
|
||||
var safePkgName = regexp.MustCompile(`^[A-Za-z0-9@._+:-]+$`)
|
||||
var safePkgName = regexp.MustCompile(`^[A-Za-z0-9@._+:/.-]+$`)
|
||||
|
||||
// shellSafeNames drops names unsafe to interpolate into the apt/zypper sh -c scripts.
|
||||
func shellSafeNames(names []string) []string {
|
||||
|
||||
@@ -59,6 +59,16 @@ func TestUpgradeCommandBuilders(t *testing.T) {
|
||||
got: dnfUpgradeArgv("dnf5", UpgradeOptions{Ignored: []string{"kernel", "mesa"}}),
|
||||
want: []string{"pkexec", "dnf5", "upgrade", "--refresh", "-y", "--exclude=kernel,mesa"},
|
||||
},
|
||||
{
|
||||
name: "portage full world upgrade",
|
||||
got: portageUpgradeArgv(pkexecOpts),
|
||||
want: []string{"pkexec", "emerge", "--update", "--newuse", "--deep", "--quiet", "@world"},
|
||||
},
|
||||
{
|
||||
name: "portage with ignored packages",
|
||||
got: portageUpgradeArgv(UpgradeOptions{Ignored: []string{"sys-apps/portage", "bad;name", "mail-client/thunderbird"}}),
|
||||
want: []string{"pkexec", "emerge", "--update", "--newuse", "--deep", "--quiet", "--exclude", "sys-apps/portage", "--exclude", "mail-client/thunderbird", "@world"},
|
||||
},
|
||||
{
|
||||
name: "apt without ignored uses plain upgrade",
|
||||
got: aptUpgradeArgv("apt-get", UpgradeOptions{}),
|
||||
|
||||
@@ -759,7 +759,7 @@ Item {
|
||||
const name = newIgnoredPackageField.value.trim();
|
||||
if (name === "")
|
||||
return;
|
||||
errorIsInvalidName = !/^[A-Za-z0-9@._+:-]+$/.test(name);
|
||||
errorIsInvalidName = !SystemUpdateService.isValidIgnoredName(name);
|
||||
if (errorIsInvalidName) {
|
||||
ignoredPackageError.visible = true;
|
||||
return;
|
||||
@@ -797,7 +797,7 @@ Item {
|
||||
SettingsRow {
|
||||
id: ignoredPackageError
|
||||
visible: false
|
||||
title: ignoredPackagesCard.errorIsInvalidName ? I18n.tr("Invalid package name — letters, digits and @._+:- only.") : I18n.tr("With Shelly, only Flatpak packages in the current update list can be ignored.")
|
||||
title: ignoredPackagesCard.errorIsInvalidName ? I18n.tr("Invalid package name — letters, digits and @._+:/- only.") : I18n.tr("With Shelly, only Flatpak packages in the current update list can be ignored.")
|
||||
titleColor: Theme.error
|
||||
}
|
||||
|
||||
|
||||
@@ -93,6 +93,10 @@ Singleton {
|
||||
return systemHoldsAllowed || pkg.repo !== "system";
|
||||
}
|
||||
|
||||
function isValidIgnoredName(name) {
|
||||
return /^[A-Za-z0-9@._+:\/-]+$/.test(name);
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DMSService
|
||||
function onCapabilitiesReceived() {
|
||||
@@ -228,14 +232,50 @@ Singleton {
|
||||
_maybeNotify();
|
||||
}
|
||||
|
||||
function _filterUpdates(pkgs) {
|
||||
function _packageMatchesIgnore(pkgName, ignored) {
|
||||
const split = (s) => {
|
||||
if (!s)
|
||||
return [s || "", ""];
|
||||
const idx = s.indexOf(":");
|
||||
if (idx === -1)
|
||||
return [s, ""];
|
||||
return [s.substring(0, idx), s.substring(idx + 1)];
|
||||
};
|
||||
const [pkgBase, pkgSlot] = split(pkgName);
|
||||
const [ignBase, ignSlot] = split(ignored);
|
||||
if (pkgSlot && ignSlot) {
|
||||
if (pkgBase === ignBase && pkgSlot === ignSlot)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
if (ignSlot && !pkgSlot) {
|
||||
if (pkgBase === ignBase && ignSlot === "0")
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
if (pkgBase === ignBase)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function _isIgnored(pkg) {
|
||||
if (!pkg || !pkg.name)
|
||||
return false;
|
||||
const ignored = SettingsData.updaterIgnoredPackages || [];
|
||||
for (let i = 0; i < ignored.length; i++) {
|
||||
if (_packageMatchesIgnore(pkg.name, ignored[i]))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function _filterUpdates(pkgs) {
|
||||
return (pkgs || []).filter(p => {
|
||||
if (!SettingsData.updaterAllowAUR && p.repo === "aur")
|
||||
return false;
|
||||
if (!canIgnorePackage(p))
|
||||
return true;
|
||||
return ignored.indexOf(p.name) === -1;
|
||||
return !_isIgnored(p);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import vm from "node:vm";
|
||||
|
||||
const source = readFileSync(new URL("../Services/SystemUpdateService.qml", import.meta.url), "utf8");
|
||||
const extract = (name) => {
|
||||
const found = source.match(new RegExp(`^ function ${name}\\([^)]*\\) \\{\\n[\\s\\S]*?^ \\}`, "m"));
|
||||
if (!found)
|
||||
throw new Error(`${name} not found in SystemUpdateService.qml`);
|
||||
return found[0];
|
||||
};
|
||||
|
||||
const scope = vm.createContext({
|
||||
SettingsData: { updaterIgnoredPackages: [], updaterAllowAUR: true },
|
||||
pkgManager: "portage",
|
||||
systemHoldsAllowed: true,
|
||||
});
|
||||
for (const fn of ["isValidIgnoredName", "_packageMatchesIgnore", "_isIgnored", "canIgnorePackage", "_filterUpdates"])
|
||||
vm.runInContext(extract(fn), scope);
|
||||
|
||||
test("portage atoms and other package names can be ignored", () => {
|
||||
for (const name of ["sys-apps/portage", "mail-client/thunderbird:0", "docker", "org.mozilla.firefox", "bash.x86_64", "gtk+"])
|
||||
assert.equal(scope.isValidIgnoredName(name), true, name);
|
||||
});
|
||||
|
||||
test("names with spaces or shell punctuation are rejected", () => {
|
||||
for (const name of ["", "sys apps", "a;b", "$(reboot)", "sys-apps/portage;reboot", "sys-apps/*"])
|
||||
assert.equal(scope.isValidIgnoredName(name), false, name);
|
||||
});
|
||||
|
||||
test("ignores match on base name when the ignore carries no slot", () => {
|
||||
const cases = [
|
||||
["gui-wm/gamescope", "gui-wm/gamescope", true],
|
||||
["app-misc/fastfetch:0", "app-misc/fastfetch", true],
|
||||
["mail-client/thunderbird:0", "mail-client/thunderbird", true],
|
||||
["gui-wm/gamescope", "gui-wm/sway", false],
|
||||
];
|
||||
for (const [pkg, ignored, want] of cases)
|
||||
assert.equal(scope._packageMatchesIgnore(pkg, ignored), want, `${pkg} vs ${ignored}`);
|
||||
});
|
||||
|
||||
test("a slot in the ignore only matches that slot, or slot 0 when the package has none", () => {
|
||||
const cases = [
|
||||
["app-misc/fastfetch:0", "app-misc/fastfetch:0", true],
|
||||
["app-misc/fastfetch:2", "app-misc/fastfetch:2", true],
|
||||
["app-misc/fastfetch:2", "app-misc/fastfetch:0", false],
|
||||
["app-misc/fastfetch", "app-misc/fastfetch:0", true],
|
||||
["app-misc/fastfetch", "app-misc/fastfetch:2", false],
|
||||
];
|
||||
for (const [pkg, ignored, want] of cases)
|
||||
assert.equal(scope._packageMatchesIgnore(pkg, ignored), want, `${pkg} vs ${ignored}`);
|
||||
});
|
||||
|
||||
test("portage ignores drop the update from the list", () => {
|
||||
scope.SettingsData.updaterIgnoredPackages = ["gui-wm/gamescope"];
|
||||
const pkgs = [
|
||||
{ name: "gui-wm/gamescope", repo: "gentoo (ebuild)", backend: "portage" },
|
||||
{ name: "mail-client/thunderbird", repo: "gentoo (ebuild)", backend: "portage" },
|
||||
];
|
||||
const kept = scope._filterUpdates(pkgs);
|
||||
assert.deepEqual(kept.map(p => p.name), ["mail-client/thunderbird"]);
|
||||
});
|
||||
Reference in new issue
Block a user