From ecdf09c63bf81ff723536687d6d6c4bdf8c7cf50 Mon Sep 17 00:00:00 2001 From: Nixietab <75538775+nixietab@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:02:57 -0300 Subject: [PATCH 1/2] Added normal up functionality --- main.go | 16 ++++++++++++---- zypper.go | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index c9f2946..9c7279d 100644 --- a/main.go +++ b/main.go @@ -33,7 +33,7 @@ type config struct { txInfo []packageInfo // parsed packages from dry-run } -// CLI +// CLI func printHelp() { // Ensure cfg is available for colorStart @@ -52,7 +52,7 @@ func printHelp() { } usage := fmt.Sprintf(`%susage%s: vlcro [-h] [-v] [-y] [-j N] [--debug] [--no-color] -%s {refresh,ref,dist-upgrade,dup,install,in,install-new-recommends,inr} ... +%s {refresh,ref,dist-upgrade,dup,update,up,install,in,install-new-recommends,inr} ... %s%s%s (%sv%s%s) makes zypper faster by running slow operations in parallel. @@ -71,6 +71,9 @@ func printHelp() { %sdist-upgrade%s (%sdup%s) perform distribution upgrade %s-d, --download-only%s download packages without installing + %supdate%s (%sup%s) update all installed packages + %s-d, --download-only%s download packages without installing + %sinstall%s (%sin%s) install one or more packages %s-d, --download-only%s download packages without installing %s%s package name(s) to install @@ -86,6 +89,7 @@ func printHelp() { hi, d, hd, d, hd, d, he, d, hd, d, hd, d, he, d, + hd, d, hd, d, he, d, hd, d, hd, d, he, d, hd, d, hd, d, hd, d, he, d) @@ -123,8 +127,8 @@ func isInstallCommand(cmd string) bool { func isKnownCommand(cmd string) bool { switch cmd { - case "refresh", "ref", "dist-upgrade", "dup", "install", "in", - "install-new-recommends", "inr": + case "refresh", "ref", "dist-upgrade", "dup", "update", "up", + "install", "in", "install-new-recommends", "inr": return true } return false @@ -340,6 +344,10 @@ func main() { if err := handleDistUpgrade(ctx, tmpDir); err != nil { exitCode = 1 } + case "update", "up": + if err := handleUpdate(ctx, tmpDir); err != nil { + exitCode = 1 + } case "install", "in": if err := handleInstall(ctx, tmpDir); err != nil { exitCode = 1 diff --git a/zypper.go b/zypper.go index 36ff478..3cbbaaf 100644 --- a/zypper.go +++ b/zypper.go @@ -742,6 +742,59 @@ func handleDistUpgrade(ctx context.Context, tmpDir string) error { return nil } +func handleUpdate(ctx context.Context, tmpDir string) error { + tracker := &MountTracker{} + defer vlcroCleanup(tmpDir, tracker) + + statusMsg("Reading package lists") + statusMsg("Building dependency tree") + info, err := dryRunAndParse([]string{"update", "--dry-run"}) + if err != nil { + errorf("%s", colorText(colorError, fmt.Sprintf("Error: %v", err))) + return err + } + + if info.nothingToDo { + infof("%s", colorText(colorInfo, "Nothing to do. Exiting...")) + return nil + } + + cfg.txInfo = info.installs + + if !showTransactionAndConfirm(info) { + return nil + } + + getZyppLock() + + if info.downloadSize > 0 { + statusMsg("Downloading packages") + err := mainTask(ctx, pkgNames(info.installs), tmpDir, false, tracker) + if err == context.Canceled { + return err + } + if err != nil { + warningf("%s", colorText(colorWarning, fmt.Sprintf("Parallel execution failed (%v). Falling back to standard zypper...", err))) + } + } + + vlcroCleanup(tmpDir, tracker) + + if !cfg.downloadOnly { + infof("%s", colorText(colorInfo, "Vlcro has finished its tasks. Handing you over to zypper...")) + nonInteractive := "" + if cfg.noConfirm { + nonInteractive = "--non-interactive" + } + code := handover(fmt.Sprintf("env ZYPP_CURL2=1 ZYPP_PCK_PRELOAD=1 ZYPP_SINGLE_RPMTRANS=1 zypper %s --no-cd update", nonInteractive)) + if code != 0 { + return fmt.Errorf("zypper update failed with exit code %d", code) + } + } + + return nil +} + func handleInstall(ctx context.Context, tmpDir string) error { if len(cfg.packages) == 0 { errorf("%s", colorText(colorError, "No packages specified for installation")) From a585b576b1d03f2bd98b0f8b179312df95ca8de1 Mon Sep 17 00:00:00 2001 From: Nixietab <75538775+nixietab@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:25:48 -0300 Subject: [PATCH 2/2] Added search passtrough with ref --- main.go | 31 ++++++++++++- zypper.go | 129 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 9c7279d..a2fce92 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "fmt" "os" "os/exec" @@ -52,7 +53,7 @@ func printHelp() { } usage := fmt.Sprintf(`%susage%s: vlcro [-h] [-v] [-y] [-j N] [--debug] [--no-color] -%s {refresh,ref,dist-upgrade,dup,update,up,install,in,install-new-recommends,inr} ... +%s {refresh,ref,dist-upgrade,dup,update,up,install,in,install-new-recommends,inr,search,se} ... %s%s%s (%sv%s%s) makes zypper faster by running slow operations in parallel. @@ -74,6 +75,9 @@ func printHelp() { %supdate%s (%sup%s) update all installed packages %s-d, --download-only%s download packages without installing + %ssearch%s (%sse%s) search for packages matching pattern + %s%s pattern(s) to search for + %sinstall%s (%sin%s) install one or more packages %s-d, --download-only%s download packages without installing %s%s package name(s) to install @@ -90,6 +94,7 @@ func printHelp() { hd, d, hd, d, he, d, hd, d, hd, d, he, d, hd, d, hd, d, he, d, + hd, d, hd, d, hd, d, hd, d, hd, d, he, d, hd, d, hd, d, hd, d, he, d) @@ -125,10 +130,14 @@ func isInstallCommand(cmd string) bool { return cmd == "install" || cmd == "in" } +func isSearchCommand(cmd string) bool { + return cmd == "search" || cmd == "se" +} + func isKnownCommand(cmd string) bool { switch cmd { case "refresh", "ref", "dist-upgrade", "dup", "update", "up", - "install", "in", "install-new-recommends", "inr": + "install", "in", "install-new-recommends", "inr", "search", "se": return true } return false @@ -142,6 +151,15 @@ func parseArgsInto(args []string) (*config, error) { for i < len(args) { arg := args[i] + + // search/se passes all remaining patterns and zypper search + // options verbatim to zypper. + if cmdFound && isSearchCommand(c.command) { + c.packages = append(c.packages, arg) + i++ + continue + } + consumedNext := false switch { @@ -348,6 +366,15 @@ func main() { if err := handleUpdate(ctx, tmpDir); err != nil { exitCode = 1 } + case "search", "se": + if err := handleSearch(ctx, tmpDir); err != nil { + var see *searchExitError + if errors.As(err, &see) { + exitCode = see.code + } else { + exitCode = 1 + } + } case "install", "in": if err := handleInstall(ctx, tmpDir); err != nil { exitCode = 1 diff --git a/zypper.go b/zypper.go index 3cbbaaf..cfe4e3d 100644 --- a/zypper.go +++ b/zypper.go @@ -10,7 +10,9 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "sort" + "strconv" "strings" "sync" "syscall" @@ -795,6 +797,133 @@ func handleUpdate(ctx context.Context, tmpDir string) error { return nil } +// search / se + +type searchExitError struct { + code int +} + +func (e *searchExitError) Error() string { + return fmt.Sprintf("zypper search failed with exit code %d", e.code) +} + +// repoRefreshDelayMinutes returns the configured repo.refresh.delay (minutes) +// from zypp.conf, or the default of 10 when unset. +func repoRefreshDelayMinutes() int { + delay := -1 + parse := func(path string) { + data, err := os.ReadFile(path) + if err != nil { + return + } + re := regexp.MustCompile(`^\s*repo\.refresh\.delay\s*[:=]\s*(\d+)\s*$`) + for _, line := range strings.Split(string(data), "\n") { + if m := re.FindStringSubmatch(line); m != nil { + if n, err := strconv.Atoi(m[1]); err == nil { + delay = n + } + } + } + } + parse("/etc/zypp/zypp.conf") + if entries, err := filepath.Glob("/etc/zypp/zypp.conf.d/*.conf"); err == nil { + sort.Strings(entries) + for _, e := range entries { + parse(e) + } + } + if delay < 0 { + delay = 10 + } + return delay +} + +// repoNeedsRefresh mirrors zypper's auto-refresh policy: a repo needs +// refreshing when its cached index file timestamp is older than +// repo.refresh.delay. Repos never refreshed must be refreshed. +func repoNeedsRefresh(alias string) bool { + delay := repoRefreshDelayMinutes() + rawPath := filepath.Join("/var/cache/zypp/raw", alias) + repomd := filepath.Join(rawPath, "repodata", "repomd.xml") + + var last time.Time + if st, err := os.Stat(repomd); err == nil { + last = st.ModTime() + } else if info, err := os.Stat(rawPath); err == nil && info.IsDir() { + filepath.Walk(rawPath, func(_ string, fi os.FileInfo, err error) error { + if err == nil && fi.ModTime().After(last) { + last = fi.ModTime() + } + return nil + }) + } else { + return true + } + if last.IsZero() { + return true + } + return time.Since(last) > time.Duration(delay)*time.Minute +} + +// runSearch runs the real zypper search with terminal passthrough so the +// output is identical to stock zypper. +func runSearch(args []string) int { + cmd := exec.Command("zypper", append([]string{"--no-refresh", "search"}, args...)...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + return exitErr.ExitCode() + } + return 1 + } + return 0 +} + +func handleSearch(ctx context.Context, tmpDir string) error { + if len(cfg.packages) == 0 { + errorf("%s", colorText(colorError, "Search must have at least one search pattern")) + return fmt.Errorf("no search pattern specified") + } + + statusMsg("Reading repository list") + xmlOutput, _ := zypperDryRun([]string{"repos"}) + repos := findElements(xmlOutput, "repo") + + var staleRepos []string + for _, r := range repos { + if r["enabled"] != "1" || r["autorefresh"] != "1" { + continue + } + if repoNeedsRefresh(r["alias"]) { + staleRepos = append(staleRepos, r["alias"]) + } + } + debugf("Stale repos: %v", staleRepos) + + if len(staleRepos) > 0 { + tracker := &MountTracker{} + statusMsg(fmt.Sprintf("Refreshing %d stale repo(s)", len(staleRepos))) + getZyppLock() + err := mainTask(ctx, staleRepos, tmpDir, true, tracker) + if err == context.Canceled { + vlcroCleanup(tmpDir, tracker) + return err + } + if err != nil { + warningf("%s", colorText(colorWarning, fmt.Sprintf("Parallel refresh failed (%v). Falling back to standard zypper...", err))) + } + vlcroCleanup(tmpDir, tracker) + } + + code := runSearch(cfg.packages) + if code != 0 { + return &searchExitError{code: code} + } + return nil +} + func handleInstall(ctx context.Context, tmpDir string) error { if len(cfg.packages) == 0 { errorf("%s", colorText(colorError, "No packages specified for installation"))