Compare commits

...

2 commits
v0.1.0 ... main

Author SHA1 Message Date
Nixietab
a585b576b1 Added search passtrough with ref 2026-09-10 16:25:48 -03:00
Nixietab
ecdf09c63b Added normal up functionality 2026-09-10 16:02:57 -03:00
2 changed files with 221 additions and 4 deletions

43
main.go
View file

@ -2,6 +2,7 @@ package main
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@ -33,7 +34,7 @@ type config struct {
txInfo []packageInfo // parsed packages from dry-run txInfo []packageInfo // parsed packages from dry-run
} }
// CLI // CLI
func printHelp() { func printHelp() {
// Ensure cfg is available for colorStart // Ensure cfg is available for colorStart
@ -52,7 +53,7 @@ func printHelp() {
} }
usage := fmt.Sprintf(`%susage%s: vlcro [-h] [-v] [-y] [-j N] [--debug] [--no-color] 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,search,se} ...
%s%s%s (%sv%s%s) makes zypper faster by running slow operations in parallel. %s%s%s (%sv%s%s) makes zypper faster by running slow operations in parallel.
@ -71,6 +72,12 @@ func printHelp() {
%sdist-upgrade%s (%sdup%s) perform distribution upgrade %sdist-upgrade%s (%sdup%s) perform distribution upgrade
%s-d, --download-only%s download packages without installing %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
%ssearch%s (%sse%s) search for packages matching pattern
%s<pattern>%s pattern(s) to search for
%sinstall%s (%sin%s) install one or more packages %sinstall%s (%sin%s) install one or more packages
%s-d, --download-only%s download packages without installing %s-d, --download-only%s download packages without installing
%s<package>%s package name(s) to install %s<package>%s package name(s) to install
@ -86,6 +93,8 @@ func printHelp() {
hi, d, 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, he, d,
hd, d, hd, d, hd, d,
hd, d, hd, d, he, d, hd, d, hd, d, hd, d, he, d, hd, d,
hd, d, hd, d, he, d) hd, d, hd, d, he, d)
@ -121,10 +130,14 @@ func isInstallCommand(cmd string) bool {
return cmd == "install" || cmd == "in" return cmd == "install" || cmd == "in"
} }
func isSearchCommand(cmd string) bool {
return cmd == "search" || cmd == "se"
}
func isKnownCommand(cmd string) bool { func isKnownCommand(cmd string) bool {
switch cmd { switch cmd {
case "refresh", "ref", "dist-upgrade", "dup", "install", "in", case "refresh", "ref", "dist-upgrade", "dup", "update", "up",
"install-new-recommends", "inr": "install", "in", "install-new-recommends", "inr", "search", "se":
return true return true
} }
return false return false
@ -138,6 +151,15 @@ func parseArgsInto(args []string) (*config, error) {
for i < len(args) { for i < len(args) {
arg := args[i] 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 consumedNext := false
switch { switch {
@ -340,6 +362,19 @@ func main() {
if err := handleDistUpgrade(ctx, tmpDir); err != nil { if err := handleDistUpgrade(ctx, tmpDir); err != nil {
exitCode = 1 exitCode = 1
} }
case "update", "up":
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": case "install", "in":
if err := handleInstall(ctx, tmpDir); err != nil { if err := handleInstall(ctx, tmpDir); err != nil {
exitCode = 1 exitCode = 1

182
zypper.go
View file

@ -10,7 +10,9 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"regexp"
"sort" "sort"
"strconv"
"strings" "strings"
"sync" "sync"
"syscall" "syscall"
@ -742,6 +744,186 @@ func handleDistUpgrade(ctx context.Context, tmpDir string) error {
return nil 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
}
// 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 { func handleInstall(ctx context.Context, tmpDir string) error {
if len(cfg.packages) == 0 { if len(cfg.packages) == 0 {
errorf("%s", colorText(colorError, "No packages specified for installation")) errorf("%s", colorText(colorError, "No packages specified for installation"))