363 lines
8.2 KiB
Go
363 lines
8.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
)
|
|
|
|
const (
|
|
vlcroVersion = "0.1.0"
|
|
zypperPidFile = "/run/zypp.pid"
|
|
maxRetries = 3
|
|
retryDelay = 100_000_000 // 100ms as int64 nanoseconds
|
|
)
|
|
|
|
var cfg *config
|
|
|
|
type config struct {
|
|
version bool
|
|
noConfirm bool
|
|
jobs int
|
|
debug bool
|
|
noColor bool
|
|
command string
|
|
force bool
|
|
downloadOnly bool
|
|
packages []string
|
|
txInfo []packageInfo // parsed packages from dry-run
|
|
}
|
|
|
|
// CLI
|
|
|
|
func printHelp() {
|
|
// Ensure cfg is available for colorStart
|
|
if cfg == nil {
|
|
cfg = &config{jobs: 10}
|
|
}
|
|
|
|
// highlight colors
|
|
hi := colorStart(colorInfo) // section headers / "vlcro"
|
|
hc := colorStart(colorWarning) // usage keyword
|
|
he := colorStart(colorError) // options
|
|
hd := colorStart(colorInput) // command bold-ish
|
|
d := "\033[0m"
|
|
if !isTerminal || cfg.noColor {
|
|
d = ""
|
|
}
|
|
|
|
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%s%s (%sv%s%s) makes zypper faster by running slow operations in parallel.
|
|
|
|
%soptions%s:
|
|
%s-h, --help%s show this help message and exit
|
|
%s-v, -V, --version%s print version number and exit
|
|
%s-y, --no-confirm%s automatic yes to prompts, run non-interactively
|
|
%s-j, --jobs%s number of parallel operations (1-64, default: 10)
|
|
%s--debug%s enable debug output
|
|
%s--no-color%s disable color output
|
|
|
|
%scommands%s:
|
|
%srefresh%s (%sref%s) refresh all enabled repos
|
|
%s-f, --force%s force a complete refresh
|
|
|
|
%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<package>%s package name(s) to install
|
|
|
|
%sinstall-new-recommends%s (%sinr%s)
|
|
install new packages recommended by already installed ones
|
|
%s-d, --download-only%s download packages without installing
|
|
`,
|
|
hc, d, hc,
|
|
hi, "vlcro", d, hi, vlcroVersion, d,
|
|
hi, d,
|
|
he, d, he, d, he, d, he, d, he, d, he, 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, hd, d, he, d)
|
|
|
|
fmt.Print(usage)
|
|
}
|
|
|
|
func parseArgs() *config {
|
|
c, err := parseArgsInto(os.Args[1:])
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
return c
|
|
}
|
|
|
|
func parseJobs(c *config, value string) error {
|
|
n, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return fmt.Errorf("Invalid jobs value: %s", value)
|
|
}
|
|
if n < 1 || n > 64 {
|
|
return fmt.Errorf("Invalid jobs value: %d. Must be between 1 and 64", n)
|
|
}
|
|
c.jobs = n
|
|
return nil
|
|
}
|
|
|
|
func isRefreshCommand(cmd string) bool {
|
|
return cmd == "refresh" || cmd == "ref"
|
|
}
|
|
|
|
func isInstallCommand(cmd string) bool {
|
|
return cmd == "install" || cmd == "in"
|
|
}
|
|
|
|
func isKnownCommand(cmd string) bool {
|
|
switch cmd {
|
|
case "refresh", "ref", "dist-upgrade", "dup", "update", "up",
|
|
"install", "in", "install-new-recommends", "inr":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func parseArgsInto(args []string) (*config, error) {
|
|
c := &config{jobs: 10}
|
|
i := 0
|
|
cmdFound := false
|
|
packagesOnly := false
|
|
|
|
for i < len(args) {
|
|
arg := args[i]
|
|
consumedNext := false
|
|
|
|
switch {
|
|
case arg == "-h" || arg == "--help":
|
|
printHelp()
|
|
os.Exit(0)
|
|
case arg == "-v" || arg == "-V" || arg == "--version":
|
|
c.version = true
|
|
i++
|
|
continue
|
|
case arg == "-y" || arg == "--no-confirm":
|
|
c.noConfirm = true
|
|
i++
|
|
continue
|
|
case arg == "--debug":
|
|
c.debug = true
|
|
i++
|
|
continue
|
|
case arg == "--no-color":
|
|
c.noColor = true
|
|
i++
|
|
continue
|
|
case arg == "-f" || arg == "--force":
|
|
c.force = true
|
|
i++
|
|
continue
|
|
case arg == "-d" || arg == "--download-only":
|
|
c.downloadOnly = true
|
|
i++
|
|
continue
|
|
case strings.HasPrefix(arg, "--"):
|
|
name, value, hasValue := strings.Cut(arg, "=")
|
|
if name == "--jobs" {
|
|
if !hasValue {
|
|
if i+1 >= len(args) {
|
|
return nil, fmt.Errorf("Error: --jobs requires an argument")
|
|
}
|
|
value = args[i+1]
|
|
i += 2
|
|
} else {
|
|
i++
|
|
}
|
|
if err := parseJobs(c, value); err != nil {
|
|
return nil, err
|
|
}
|
|
continue
|
|
}
|
|
return nil, fmt.Errorf("Unknown option: %s", arg)
|
|
case strings.HasPrefix(arg, "-") && len(arg) > 1:
|
|
body := arg[1:]
|
|
for k := 0; k < len(body); k++ {
|
|
switch body[k] {
|
|
case 'h':
|
|
printHelp()
|
|
os.Exit(0)
|
|
case 'v', 'V':
|
|
c.version = true
|
|
case 'y':
|
|
c.noConfirm = true
|
|
case 'f':
|
|
c.force = true
|
|
case 'd':
|
|
c.downloadOnly = true
|
|
case 'j':
|
|
rest := body[k+1:]
|
|
if strings.HasPrefix(rest, "=") {
|
|
rest = rest[1:]
|
|
}
|
|
if rest == "" {
|
|
if i+1 >= len(args) {
|
|
return nil, fmt.Errorf("Error: --jobs requires an argument")
|
|
}
|
|
rest = args[i+1]
|
|
consumedNext = true
|
|
}
|
|
if err := parseJobs(c, rest); err != nil {
|
|
return nil, err
|
|
}
|
|
k = len(body)
|
|
default:
|
|
return nil, fmt.Errorf("Unknown option: -%s", string(body[k]))
|
|
}
|
|
}
|
|
if !consumedNext {
|
|
i++
|
|
} else {
|
|
i += 2
|
|
}
|
|
continue
|
|
default:
|
|
if !cmdFound {
|
|
c.command = arg
|
|
cmdFound = true
|
|
packagesOnly = isInstallCommand(arg)
|
|
i++
|
|
continue
|
|
}
|
|
if !packagesOnly {
|
|
return nil, fmt.Errorf("Unexpected argument: %s", arg)
|
|
}
|
|
c.packages = append(c.packages, arg)
|
|
i++
|
|
continue
|
|
}
|
|
}
|
|
|
|
if !cmdFound {
|
|
return c, nil
|
|
}
|
|
|
|
if !isKnownCommand(c.command) {
|
|
return nil, fmt.Errorf("Unknown command: %s", c.command)
|
|
}
|
|
if c.force && !isRefreshCommand(c.command) {
|
|
return nil, fmt.Errorf("Option --force is only valid with the refresh command")
|
|
}
|
|
if c.downloadOnly && isRefreshCommand(c.command) {
|
|
return nil, fmt.Errorf("Option --download-only is not valid with the refresh command")
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
// dependency check (exec.LookPath)
|
|
|
|
func checkDependencies() {
|
|
required := []string{"zypper", "findmnt", "bash"}
|
|
missing := []string{}
|
|
for _, program := range required {
|
|
if _, err := exec.LookPath(program); err != nil {
|
|
missing = append(missing, program)
|
|
}
|
|
}
|
|
if len(missing) > 0 {
|
|
msg := fmt.Sprintf("Bailing out, missing required dependencies: %s\n"+
|
|
"The following are required for vlcro to function: %s",
|
|
strings.Join(missing, ", "),
|
|
strings.Join(required, ", "))
|
|
errorf("%s", colorText(colorError, msg))
|
|
os.Exit(4)
|
|
}
|
|
}
|
|
|
|
// main
|
|
|
|
func main() {
|
|
cfg = parseArgs()
|
|
|
|
if cfg.version {
|
|
fmt.Printf("vlcro v%s\n", vlcroVersion)
|
|
return
|
|
}
|
|
if cfg.command == "" {
|
|
printHelp()
|
|
return
|
|
}
|
|
|
|
if os.Getuid() != 0 {
|
|
errorf("%s", colorText(colorError, "Bailing out, program must be run with root privileges"))
|
|
os.Exit(3)
|
|
}
|
|
|
|
checkDependencies()
|
|
|
|
if pid, program := checkZypperRunning(); pid != 0 {
|
|
msg := fmt.Sprintf("zypper is already invoked by the application with pid %d (%s).\nClose this application before trying again.", pid, program)
|
|
errorf("%s", colorText(colorError, msg))
|
|
os.Exit(5)
|
|
}
|
|
|
|
tmpDir, err := os.MkdirTemp("/tmp", "vlcro_")
|
|
if err != nil {
|
|
errorf("%s", colorText(colorError, fmt.Sprintf("Failed to create temp directory: %v", err)))
|
|
os.Exit(1)
|
|
}
|
|
|
|
var exitCode int
|
|
defer func() {
|
|
os.Exit(exitCode)
|
|
}()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
sigChan := make(chan os.Signal, 1)
|
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
|
go func() {
|
|
<-sigChan
|
|
infof("%s", colorText(colorInfo, "Cancelling pending tasks..."))
|
|
os.Stdin.Close()
|
|
cancel()
|
|
}()
|
|
|
|
switch cfg.command {
|
|
case "refresh", "ref":
|
|
if err := handleRefresh(ctx, tmpDir); err != nil {
|
|
exitCode = 1
|
|
}
|
|
case "dist-upgrade", "dup":
|
|
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
|
|
}
|
|
case "install-new-recommends", "inr":
|
|
if err := handleINR(ctx, tmpDir); err != nil {
|
|
exitCode = 1
|
|
}
|
|
default:
|
|
printHelp()
|
|
exitCode = 1
|
|
}
|
|
}
|