diff options
Diffstat (limited to 'src/pacman.nim')
-rw-r--r-- | src/pacman.nim | 367 |
1 files changed, 367 insertions, 0 deletions
diff --git a/src/pacman.nim b/src/pacman.nim new file mode 100644 index 0000000..c457f49 --- /dev/null +++ b/src/pacman.nim @@ -0,0 +1,367 @@ +import + future, macros, options, os, posix, re, sequtils, sets, strutils, tables, + args, config, utils + +type + OpGroup* {.pure.} = enum + syncInstall, syncSearch, syncQuery, localQuery + + OperationType* {.pure.} = enum + unknown, invalid, database, files, query, + remove, sync, deptest, upgrade + + Operation* = tuple[ + pair: OptionPair, + otype: OperationType + ] + + CommandOption* = tuple[ + pair: OptionPair, + hasParam: bool, + extension: bool, + groups: set[OpGroup] + ] + + ConflictingOptions* = tuple[ + left: string, + right: seq[string] + ] + +proc calculateOptionsWithParameter(opts: seq[CommandOption]): seq[OptionKey] {.compileTime.} = + proc commandToSeq(co: CommandOption): seq[OptionKey] {.compileTime.} = + if co.hasParam: + co.pair.short + .map(s => @[(s, false), (co.pair.long, true)]) + .get(@[(co.pair.long, true)]) + else: + @[] + + lc[x | (y <- opts, x <- commandToSeq(y)), OptionKey] + +proc o(long: string): CommandOption {.compileTime.} = + ((none(string), long), false, false, {}) + +proc o(short: string, long: string): CommandOption {.compileTime.} = + ((some(short), long), false, false, {}) + +proc `^`(opt: CommandOption): CommandOption {.compileTime.} = + (opt.pair, not opt.hasParam, opt.extension, opt.groups) + +proc `$`(opt: CommandOption): CommandOption {.compileTime.} = + (opt.pair, opt.hasParam, not opt.extension, opt.groups) + +proc `+`(opt: CommandOption, groups: set[OpGroup]): CommandOption {.compileTime.} = + (opt.pair, opt.hasParam, opt.extension, opt.groups + groups) + +macro g(gls: varargs[untyped]): untyped = + result = newNimNode(nnkCurly, gls) + for gl in gls: + add(result, newDotExpr(ident("OpGroup"), gl)) + +const + operations*: seq[Operation] = @[ + ((some("D"), "database"), OperationType.database), + ((some("F"), "files"), OperationType.files), + ((some("Q"), "query"), OperationType.query), + ((some("R"), "remove"), OperationType.remove), + ((some("S"), "sync"), OperationType.sync), + ((some("T"), "deptest"), OperationType.deptest), + ((some("U"), "upgrade"), OperationType.upgrade) + ] + + commonOptions*: seq[CommandOption] = @[ + ^o("b", "dbpath"), + ^o("r", "root"), + o("v", "verbose"), + ^o("arch"), + ^o("cachedir"), + ^o("color"), + ^o("config"), + o("debug"), + ^o("gpgdir"), + ^o("hookdir"), + ^o("logfile"), + o("noconfirm"), + o("confirm") + ] + + transactionOptions*: seq[CommandOption] = @[ + o("d", "nodeps"), + ^o("assume-installed"), + o("dbonly"), + o("noprogressbar"), + o("noscriptlet"), + o("p", "print"), + ^o("print-format") + ] + + upgradeOptions*: seq[CommandOption] = @[ + o("force"), + o("asdeps"), + o("asexplicit"), + ^o("ignore"), + ^o("ignoregroup"), + o("needed") + ] + + queryOptions*: seq[CommandOption] = @[ + o("c", "changelog") + g(localQuery), + o("d", "deps") + g(localQuery), + o("e", "explicit") + g(localQuery), + o("g", "groups"), + o("i", "info") + g(localQuery), + o("k", "check") + g(localQuery), + o("l", "list") + g(localQuery), + o("m", "foreign") + g(localQuery), + o("n", "native") + g(localQuery), + o("o", "owns"), + o("p", "file"), + o("q", "quiet") + g(localQuery), + o("s", "search"), + o("t", "unrequired") + g(localQuery), + o("u", "upgrades") + g(localQuery) + ] + + removeOptions*: seq[CommandOption] = @[ + o("c", "cascade"), + o("n", "nosave"), + o("s", "recursive"), + o("u", "unneeded") + ] + + syncOptions*: seq[CommandOption] = @[ + o("c", "clean"), + o("g", "groups"), + o("i", "info") + g(syncInstall, syncQuery), + o("l", "list"), + o("q", "quiet") + g(syncInstall, syncSearch, syncQuery), + o("s", "search") + g(syncSearch), + o("u", "sysupgrade") + g(syncInstall), + o("w", "downloadonly"), + o("y", "refresh") + g(syncInstall, syncSearch, syncQuery), + $o("build") + g(syncInstall), + $o("noaur") + g(syncInstall) + ] + + databaseOptions*: seq[CommandOption] = @[ + o("asdeps"), + o("asexplicit"), + o("k", "check") + ] + + filesOptions*: seq[CommandOption] = @[ + o("y", "refresh"), + o("l", "list"), + o("s", "search"), + o("x", "regex"), + o("o", "owns"), + o("q", "quiet"), + o("machinereadable") + ] + + upgradeCommonOptions*: seq[CommandOption] = @[ + o("noprogressbar"), + o("force") + ] + + allOptions = commonOptions & transactionOptions & + upgradeOptions & queryOptions & removeOptions & syncOptions & + databaseOptions & filesOptions + + optionsWithParameter*: HashSet[OptionKey] = + calculateOptionsWithParameter(allOptions).toSet + + syncConflictingOptions*: seq[ConflictingOptions] = @[ + ("asdeps", @["asexplicit"]), + ("build", @["nodeps", "assume-installed", "dbonly", "clean", + "groups", "info", "list", "search", "sysupgrade", "downloadonly"]) + ] + + allConflictingOptions = syncConflictingOptions + +proc checkOptions(check: seq[CommandOption], + where: openArray[seq[CommandOption]]) {.compileTime.} = + let whereSeq = @where + let whereSet = lc[x.pair | (y <- whereSeq, x <- y), OptionPair].toSet + for c in check: + if not (c.pair in whereSet): + raise newException(SystemError, + "invalid options definition: " & $c.pair) + +static: + # options test + checkOptions(upgradeCommonOptions, [commonOptions, transactionOptions, upgradeOptions]) + +proc getOperation*(args: seq[Argument]): OperationType = + let matchedOps = args + .map(arg => operations + .filter(o => (arg.isShort and some(arg.key) == o.pair.short) or + (arg.isLong and arg.key == o.pair.long))) + .filter(ops => ops.len > 0) + + if matchedOps.len == 0: + OperationType.unknown + elif matchedOps.len == 1: + matchedOps[0][0].otype + else: + OperationType.invalid + +proc filterOptions*(args: seq[Argument], removeMatches: bool, keepTargets: bool, + includeOptions: bool, opts: varargs[seq[CommandOption]]): seq[Argument] = + let optsSeq = @opts + let optsPairSeq = lc[x.pair | (y <- optsSeq, x <- y), OptionPair] + + let work = if includeOptions: + (optsPairSeq & operations.map(o => o.pair)) + else: + optsPairSeq + + args.filter(removeMatches, keepTargets, work) + +template removeMatchOptions*(args: seq[Argument], + opts: varargs[seq[CommandOption]]): seq[Argument] = + filterOptions(args, true, true, true, opts) + +template keepOnlyOptions*(args: seq[Argument], + opts: varargs[seq[CommandOption]]): seq[Argument] = + filterOptions(args, false, false, false, opts) + +proc checkValid*(args: seq[Argument], opts: varargs[seq[CommandOption]]): bool = + filterOptions(args, true, false, true, opts).len == 0 + +proc checkOpGroup*(args: seq[Argument], group: OpGroup): bool = + let toCheck = allOptions + .filter(o => group in o.groups) + .map(o => o.pair) + + args.whitelisted(toCheck) + +proc filterExtensions*(args: seq[Argument], + removeMatches: bool, keepTargets: bool): seq[Argument] = + let argsSeq = lc[x.pair | (x <- allOptions, x.extension), OptionPair] + args.filter(removeMatches, keepTargets, argsSeq) + +proc obtainConflictsPairs(conflicts: seq[ConflictingOptions]): Table[string, seq[OptionPair]] = + let all = lc[x | (y <- conflicts, x <- y.left & y.right), string].deduplicate + all.map(c => (c, allOptions.filter(o => o.pair.long == c) + .map(o => o.pair).deduplicate)).toTable + +static: + # conflicting options test + for name, pairs in allConflictingOptions.obtainConflictsPairs: + if pairs.len != 1: + raise newException(SystemError, + "invalid conflicts definition: " & name & " " & $pairs) + +proc checkConflicts*(args: seq[Argument], + conflicts: seq[ConflictingOptions]): Option[(string, string)] = + let table = conflicts.obtainConflictsPairs + template full(s: string): OptionPair = table[s][0] + + lc[(c.left, w) | (c <- conflicts, args.check(c.left.full), + w <- c.right, args.check(w.full)), (string, string)].optFirst + +proc checkExec(file: string): bool = + var statv: Stat + stat(file, statv) == 0 and (statv.st_mode and S_IXUSR) == S_IXUSR + +proc pacmanExec(root: bool, args: varargs[string]): int = + let exec = if root and checkExec(sudoCmd): + @[sudoCmd, pacmanCmd] & @args + elif root and checkExec(suCmd): + @[suCmd, "root", "-c", "exec \"$@\"", "--", "sh", pacmanCmd] & @args + else: + @[pacmanCmd] & @args + + execResult(exec) + +proc pacmanExec*(root: bool, color: bool, args: varargs[Argument]): int = + let useRoot = root and getuid() != 0 + let colorStr = if color: "always" else: "never" + + let argsSeq = ("color", some(colorStr), ArgumentType.long) & + @args.filter(arg => not arg.matchOption((none(string), "color"))) + let collectedArgs = lc[x | (y <- argsSeq, x <- y.collectArg), string] + + pacmanExec(useRoot, collectedArgs) + +proc pacmanRun*(root: bool, color: bool, args: varargs[Argument]): int = + let argsSeq = @args + forkWait(() => pacmanExec(root, color, argsSeq)) + +proc pacmanValidateAndThrow(args: varargs[Argument]): void = + let argsSeq = @args + let collectedArgs = lc[x | (y <- argsSeq, x <- y.collectArg), string] + let code = forkWait(() => pacmanExec(false, "-T" & collectedArgs)) + if code != 0: + raise haltError(code) + +proc getMachineName: Option[string] = + var utsname: Utsname + let length = if uname(utsname) == 0: utsname.machine.find('\0') else: -1 + if length > 0: some(utsname.machine.toString(some(length))) else: none(string) + +proc createConfigFromTable(table: Table[string, string], dbs: seq[string]): PacmanConfig = + let root = table.opt("RootDir") + let db = table.opt("DBPath") + let color = if table.hasKey("Color"): ColorMode.colorAuto else: ColorMode.colorNever + let verbosePkgList = table.hasKey("VerbosePkgLists") + let arch = table.opt("Architecture").get("auto") + let ignorePkgs = table.opt("IgnorePkg").get("").splitWhitespace.toSet + let ignoreGroups = table.opt("IgnoreGroup").get("").splitWhitespace.toSet + + let archFinal = if arch.len == 0 or arch == "auto": getMachineName().get(arch) else: arch + if archFinal.len == 0 or archFinal == "auto": + raise commandError(tr"can not get the architecture", + colorNeeded = some(color.get)) + + PacmanConfig(rootOption: root, dbOption: db, dbs: dbs, + arch: archFinal, colorMode: color, debug: false, + progressBar: true, verbosePkgList: verbosePkgList, + ignorePkgs: ignorePkgs, ignoreGroups: ignoreGroups) + +proc obtainPacmanConfig*(args: seq[Argument]): PacmanConfig = + proc getAll(pair: OptionPair): seq[string] = + args.filter(arg => arg.matchOption(pair)).map(arg => arg.value.get) + + let configFile = getAll((none(string), "config")).optLast.get(sysConfDir & "/pacman.conf") + let (configTable, wasError) = readConfigFile(configFile) + + let options = configTable.opt("options").map(t => t[]).get(initTable[string, string]()) + let dbs = toSeq(configTable.keys).filter(k => k != "options") + let defaultConfig = createConfigFromTable(options, dbs) + + if wasError: + pacmanValidateAndThrow(("config", some(configFile), ArgumentType.long)) + + proc getColor(color: string): ColorMode = + let colors = toSeq(enumerate[ColorMode]()) + colors.filter(c => $c == color).optLast.get(ColorMode.colorNever) + + let root = getAll((some("r"), "root")).optLast.orElse(defaultConfig.rootOption) + let db = getAll((some("b"), "dbpath")).optLast.orElse(defaultConfig.dbOption) + let arch = getAll((none(string), "arch")).optLast.get(defaultConfig.arch) + let colorStr = getAll((none(string), "color")).optLast.get($defaultConfig.colorMode) + let color = getColor(colorStr) + + let debug = args.check((none(string), "debug")) + let progressBar = not args.check((none(string), "noprogressbar")) + let ignorePkgs = getAll((none(string), "ignore")).toSet + let ignoreGroups = getAll((none(string), "ignoregroups")).toSet + + let config = PacmanConfig(rootOption: root, dbOption: db, dbs: defaultConfig.dbs, + arch: arch, colorMode: color, debug: debug, + progressBar: progressBar, verbosePkgList: defaultConfig.verbosePkgList, + ignorePkgs: ignorePkgs + defaultConfig.ignorePkgs, + ignoreGroups: ignoreGroups + defaultConfig.ignoreGroups) + + if config.dbs.find("aur") >= 0: + raise commandError(tr"repo '$#' is reserved by this program" % ["aur"], + colorNeeded = some(color.get)) + + pacmanValidateAndThrow(("root", some(config.root), ArgumentType.long), + ("dbpath", some(config.db), ArgumentType.long), + ("arch", some(config.arch), ArgumentType.long), + ("color", some(colorStr), ArgumentType.long)) + + config |