Compare commits
No commits in common. "main" and "v0.1.0" have entirely different histories.
2 changed files with 4 additions and 221 deletions
41
main.go
41
main.go
|
|
@ -2,7 +2,6 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
@ -53,7 +52,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,update,up,install,in,install-new-recommends,inr,search,se} ...
|
%s {refresh,ref,dist-upgrade,dup,install,in,install-new-recommends,inr} ...
|
||||||
|
|
||||||
%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.
|
||||||
|
|
||||||
|
|
@ -72,12 +71,6 @@ 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
|
||||||
|
|
@ -93,8 +86,6 @@ 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)
|
||||||
|
|
||||||
|
|
@ -130,14 +121,10 @@ 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", "update", "up",
|
case "refresh", "ref", "dist-upgrade", "dup", "install", "in",
|
||||||
"install", "in", "install-new-recommends", "inr", "search", "se":
|
"install-new-recommends", "inr":
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|
@ -151,15 +138,6 @@ 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 {
|
||||||
|
|
@ -362,19 +340,6 @@ 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
182
zypper.go
|
|
@ -10,9 +10,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
@ -744,186 +742,6 @@ 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"))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue