aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorkitsunyan2018-03-10 21:10:43 +0000
committerkitsunyan2018-03-10 21:10:43 +0000
commitfd397b356e9be1d3572ace965ead57120803a0ac (patch)
treed9e6fe9f903ab935fa6df2e6862a97515caf926c /src
Initial commitv0.1
Diffstat (limited to 'src')
-rw-r--r--src/args.nim175
-rw-r--r--src/aur.nim192
-rw-r--r--src/common.nim392
-rw-r--r--src/config.nim125
-rw-r--r--src/feature/localquery.nim81
-rw-r--r--src/feature/syncinfo.nim130
-rw-r--r--src/feature/syncinstall.nim827
-rw-r--r--src/feature/syncsearch.nim53
-rw-r--r--src/format.nim308
-rw-r--r--src/main.nim260
-rw-r--r--src/package.nim249
-rw-r--r--src/pacman.nim367
-rw-r--r--src/utils.nim203
-rw-r--r--src/wrapper/alpm.nim146
-rw-r--r--src/wrapper/curl.nim123
15 files changed, 3631 insertions, 0 deletions
diff --git a/src/args.nim b/src/args.nim
new file mode 100644
index 0000000..12b31b3
--- /dev/null
+++ b/src/args.nim
@@ -0,0 +1,175 @@
+import
+ future, options, os, posix, sequtils, sets, strutils,
+ utils
+
+type
+ ArgumentType* {.pure.} = enum
+ short, long, target
+
+ Argument* = tuple[
+ key: string,
+ value: Option[string],
+ atype: ArgumentType
+ ]
+
+ OptionPair* = tuple[
+ short: Option[string],
+ long: string
+ ]
+
+ OptionKey* = tuple[
+ key: string,
+ long: bool
+ ]
+
+iterator readLines(): string =
+ try:
+ while true:
+ yield readLine(stdin)
+ except:
+ discard
+
+iterator splitSingle(valueFull: string, optionsWithParameter: HashSet[OptionKey],
+ next: Option[string]): tuple[key: string, value: Option[string], consumedNext: bool] =
+ var i = 0
+ while i < valueFull.len:
+ let key = $valueFull[i]
+ if (key, false) in optionsWithParameter:
+ if i == valueFull.high:
+ if next.isNone:
+ raise commandError(trc("%s: option requires an argument -- '%c'\n").strip
+ .replace("%s", "$#").replace("%c", "$#") % [getAppFilename(), key],
+ showError = false)
+ else:
+ yield (key, next, true)
+ else:
+ yield (key, some(valueFull[i + 1 .. ^1]), false)
+ i = valueFull.len
+ else:
+ yield (key, none(string), false)
+ i += 1
+
+proc splitArgs*(params: seq[string],
+ optionsWithParameter: HashSet[OptionKey]): seq[Argument] =
+ proc handleCurrentNext(current: string, next: Option[string],
+ stdinConsumed: bool, endOfOpts: bool): (seq[Argument], Option[string], bool, bool) =
+ if current == "-":
+ if stdinConsumed or isatty(0) == 1:
+ raise commandError(trp("argument '-' specified without input on stdin\n").strip)
+ else:
+ let args = lc[x | (y <- readLines(), x <- y.splitWhitespace), string]
+ .map(s => (s, none(string), ArgumentType.target))
+
+ return (args, next, true, endOfOpts)
+ elif endOfOpts:
+ return (@[(current, none(string), ArgumentType.target)], next, stdinConsumed, true)
+ elif current == "--":
+ return (@[], next, stdinConsumed, true)
+ elif current[0 .. 1] == "--":
+ let valueFull = current[2 .. ^1]
+ let index = valueFull.find("=")
+ let key = if index >= 0: valueFull[0 .. index - 1] else: valueFull
+ let valueOption = if index >= 0: some(valueFull[index + 1 .. ^1]) else: none(string)
+
+ if (key, true) in optionsWithParameter:
+ if valueOption.isSome:
+ return (@[(key, valueOption, ArgumentType.long)], next, stdinConsumed, false)
+ elif next.isSome:
+ return (@[(key, next, ArgumentType.long)], none(string), stdinConsumed, false)
+ else:
+ raise commandError(trc("%s: option '%s%s' requires an argument\n").strip
+ .replace("%s", "$#") % [getAppFilename(), "--", key], showError = false)
+ elif valueOption.isSome:
+ raise commandError(trc("%s: option '%s%s' doesn't allow an argument\n").strip
+ .replace("%s", "$#") % [getAppFilename(), "--", key], showError = false)
+ else:
+ return (@[(key, none(string), ArgumentType.long)], next, stdinConsumed, false)
+ elif current[0] == '-' and current.len >= 2:
+ let argsResult = toSeq(splitSingle(current[1 .. ^1], optionsWithParameter, next))
+ let consumedNext = argsResult.map(a => a.consumedNext).foldl(a or b)
+ let newNext = next.filter(n => not consumedNext)
+
+ return (lc[(x.key, x.value, ArgumentType.short) | (x <- argsResult), Argument],
+ newNext, stdinConsumed, false)
+ else:
+ return (@[(current, none(string), ArgumentType.target)], next, stdinConsumed, false)
+
+ type
+ ParseArgument = tuple[arg: Option[Argument],current: Option[string]]
+ ParseCycle = tuple[args: seq[ParseArgument], stdinConsumed: bool, endOfOpts: bool]
+
+ proc buildArgs(input: ParseCycle, next: Option[string]): ParseCycle =
+ if input.args.len == 0:
+ (@[(none(Argument), next)], input.stdinConsumed, input.endOfOpts)
+ else:
+ let last = input.args[^1]
+ if last.current.isSome:
+ let handleResult: tuple[args: seq[Argument], next: Option[string],
+ stdinConsumed: bool, endOfOpts: bool] = handleCurrentNext(last.current.unsafeGet,
+ next, input.stdinConsumed, input.endOfOpts)
+
+ let append = handleResult.args.map(a => (some(a), none(string)))
+ (input.args[0 .. ^2] & append & (none(Argument), handleResult.next),
+ handleResult.stdinConsumed, handleResult.endOfOpts)
+ elif next.isSome:
+ (input.args & (none(Argument), next), input.stdinConsumed, input.endOfOpts)
+ else:
+ input
+
+ let cycle: ParseCycle = (params.map(some) & none(string))
+ .foldl(buildArgs(a, b), (newSeq[ParseArgument](), false, false))
+
+ if cycle.stdinConsumed:
+ discard close(0)
+ discard open("/dev/tty", O_RDONLY)
+
+ lc[x | (y <- cycle.args, x <- y.arg), Argument]
+
+proc isShort*(arg: Argument): bool = arg.atype == ArgumentType.short
+proc isLong*(arg: Argument): bool = arg.atype == ArgumentType.long
+proc isTarget*(arg: Argument): bool = arg.atype == ArgumentType.target
+
+proc collectArg*(arg: Argument): seq[string] =
+ if arg.isShort:
+ let key = "-" & arg.key
+ arg.value.map(v => @[key, v]).get(@[key])
+ elif arg.isLong:
+ let key = "--" & arg.key
+ arg.value.map(v => @[key, v]).get(@[key])
+ elif arg.isTarget:
+ @[arg.key]
+ else:
+ @[]
+
+proc len*(op: OptionPair): int =
+ if op.short.isSome: 2 else: 1
+
+iterator items*(op: OptionPair): OptionKey =
+ if op.short.isSome:
+ yield (op.short.unsafeGet, false)
+ yield (op.long, true)
+
+proc filter*(args: seq[Argument], removeMatches: bool, keepTargets: bool,
+ pairs: varargs[OptionPair]): seq[Argument] =
+ let pairsSeq = @pairs
+ let argsSet = lc[x | (y <- pairsSeq, x <- y), OptionKey].toSet
+
+ args.filter(arg => (arg.isShort and (removeMatches xor (arg.key, false) in argsSet)) or
+ (arg.isLong and (removeMatches xor (arg.key, true) in argsSet)) or
+ (arg.isTarget and keepTargets))
+
+template count*(args: seq[Argument], pairs: varargs[OptionPair]): int =
+ args.filter(false, false, pairs).len
+
+template check*(args: seq[Argument], pairs: varargs[OptionPair]): bool =
+ args.count(pairs) > 0
+
+template whitelisted*(args: seq[Argument], pairs: varargs[OptionPair]): bool =
+ args.filter(true, false, pairs).len == 0
+
+proc matchOption*(arg: Argument, pair: OptionPair): bool =
+ (arg.isShort and pair.short.map(o => o == arg.key).get(false)) or
+ (arg.isLong and arg.key == pair.long)
+
+proc targets*(args: seq[Argument]): seq[string] =
+ args.filter(isTarget).map(a => a.key)
diff --git a/src/aur.nim b/src/aur.nim
new file mode 100644
index 0000000..acf9f96
--- /dev/null
+++ b/src/aur.nim
@@ -0,0 +1,192 @@
+import
+ future, json, options, re, sequtils, sets, strutils, tables,
+ package, utils,
+ "wrapper/curl"
+
+type
+ AurComment* = tuple[
+ author: string,
+ date: string,
+ text: string
+ ]
+
+const
+ aurUrl* = "https://aur.archlinux.org/"
+
+proc parseRpcPackageInfo(obj: JsonNode): Option[RpcPackageInfo] =
+ template optInt64(i: int64): Option[int64] =
+ if i > 0: some(i) else: none(int64)
+
+ let base = obj["PackageBase"].getStr
+ let name = obj["Name"].getStr
+ let version = obj["Version"].getStr
+ let descriptionEmpty = obj["Description"].getStr
+ let description = if descriptionEmpty.len > 0: some(descriptionEmpty) else: none(string)
+ let maintainerEmpty = obj["Maintainer"].getStr
+ let maintainer = if maintainerEmpty.len > 0: some(maintainerEmpty) else: none(string)
+ let firstSubmitted = obj["FirstSubmitted"].getBiggestInt(0).optInt64
+ let lastModified = obj["LastModified"].getBiggestInt(0).optInt64
+ let votes = (int) obj["NumVotes"].getBiggestInt(0)
+ let popularity = obj["Popularity"].getFloat(0)
+
+ if base.len > 0 and name.len > 0:
+ some(RpcPackageInfo(repo: "aur", base: base, name: name, version: version,
+ description: description, maintainer: maintainer,
+ firstSubmitted: firstSubmitted, lastModified: lastModified,
+ votes: votes, popularity: popularity))
+ else:
+ none(RpcPackageInfo)
+
+template withAur*(body: untyped): untyped =
+ withCurlGlobal():
+ body
+
+proc obtainPkgBaseSrcInfo(base: string): (string, Option[string]) =
+ try:
+ withAur():
+ withCurl(instance):
+ let url = aurUrl & "cgit/aur.git/plain/.SRCINFO?h=" &
+ instance.escape(base)
+ (performString(url), none(string))
+ except CurlError:
+ ("", some(getCurrentException().msg))
+
+proc getRpcPackageInfo*(pkgs: seq[string]): (seq[RpcPackageInfo], Option[string]) =
+ if pkgs.len == 0:
+ (@[], none(string))
+ else:
+ withAur():
+ try:
+ withCurl(instance):
+ let url = aurUrl & "rpc/?v=5&type=info&arg[]=" & @pkgs
+ .deduplicate
+ .map(u => instance.escape(u))
+ .foldl(a & "&arg[]=" & b)
+
+ let response = performString(url)
+ let results = parseJson(response)["results"]
+ let table = lc[(x.name, x) | (y <- results, x <- parseRpcPackageInfo(y)),
+ (string, RpcPackageInfo)].toTable
+ (lc[x | (p <- pkgs, x <- table.opt(p)), RpcPackageInfo], none(string))
+ except CurlError:
+ (@[], some(getCurrentException().msg))
+ except JsonParsingError:
+ (@[], some(tr"failed to parse server response"))
+
+proc getAurPackageInfo*(pkgs: seq[string], rpcInfosOption: Option[seq[RpcPackageInfo]],
+ progressCallback: (int, int) -> void): (seq[PackageInfo], seq[string]) =
+ if pkgs.len == 0:
+ (@[], @[])
+ else:
+ withAur():
+ progressCallback(0, pkgs.len)
+
+ let (rpcInfos, error) = if rpcInfosOption.isSome:
+ (rpcInfosOption.unsafeGet, none(string))
+ else:
+ getRpcPackageInfo(pkgs)
+
+ if error.isSome:
+ (@[], @[error.unsafeGet])
+ else:
+ type
+ ParseResult = tuple[
+ infos: seq[PackageInfo],
+ error: Option[string]
+ ]
+
+ let deduplicated = lc[x.base | (x <- rpcInfos), string].deduplicate
+ progressCallback(0, deduplicated.len)
+
+ proc obtainAndParse(base: string, index: int): ParseResult =
+ let (srcInfo, operror) = obtainPkgBaseSrcInfo(base)
+ progressCallback(index + 1, deduplicated.len)
+
+ if operror.isSome:
+ (@[], operror)
+ else:
+ let pkgInfos = parseSrcInfo("aur", srcInfo,
+ aurUrl & base & ".git", none(string), none(string), none(string), rpcInfos)
+ (pkgInfos, none(string))
+
+ let parsed = deduplicated.foldl(a & obtainAndParse(b, a.len), newSeq[ParseResult]())
+ let infos = lc[x | (y <- parsed, x <- y.infos), PackageInfo]
+ let errors = lc[x | (y <- parsed, x <- y.error), string]
+
+ let table = infos.map(i => (i.name, i)).toTable
+ (lc[x | (p <- pkgs, x <- table.opt(p)), PackageInfo], errors)
+
+proc findAurPackages*(query: seq[string]): (seq[RpcPackageInfo], Option[string]) =
+ if query.len == 0 or query[0].len <= 2:
+ (@[], none(string))
+ else:
+ withAur():
+ try:
+ withCurl(instance):
+ let url = aurUrl & "rpc/?v=5&type=search&by=name&arg=" &
+ instance.escape(query[0])
+
+ let response = performString(url)
+ let results = parseJson(response)["results"]
+ let rpcInfos = lc[x | (y <- results, x <- parseRpcPackageInfo(y)), RpcPackageInfo]
+
+ let filteredRpcInfos = if query.len > 1: (block:
+ let queryLow = query[1 .. ^1].map(q => q.toLowerAscii)
+ rpcInfos.filter(i => queryLow.map(q => i.name.toLowerAscii.contains(q) or
+ i.description.map(d => d.toLowerAscii.contains(q)).get(false)).foldl(a and b)))
+ else:
+ rpcInfos
+
+ (filteredRpcInfos, none(string))
+ except CurlError:
+ (@[], some(getCurrentException().msg))
+
+proc downloadAurComments*(base: string): (seq[AurComment], Option[string]) =
+ let (content, error) = withAur():
+ try:
+ withCurl(instance):
+ let url = aurUrl & "pkgbase/" & base & "/?comments=all"
+ (performString(url), none(string))
+ except CurlError:
+ ("", some(getCurrentException().msg))
+
+ if error.isSome:
+ (@[], error)
+ else:
+ let commentRe = re("<h4\\ id=\"comment-\\d+\">\\n\\s+(.*)?\\ commented\\ on\\ " &
+ "(.*)\\n(?:.*\\n)*?\\s+</h4>\\n\\t\\t<div\\ id=\"comment-\\d+-content\"\\ " &
+ "class=\"article-content\">((?:\\n.*)*?)\\n\\t\\t</div>")
+
+ proc transformComment(comment: string): string =
+ comment
+ # line breaks can leave a space
+ .replace("\n", " ")
+ # force line break
+ .replace("<br />", "\n")
+ # paragraphs look like 2 line breaks
+ .replace("<p>", "\n\n")
+ .replace("</p>", "\n\n")
+ # remove tags
+ .replace(re"<.*?>", "")
+ # multiple spaces become 1 spage
+ .replace(re"\ {2,}", " ")
+ # strip lines
+ .strip.split("\n").map(s => s.strip).foldl(a & "\n" & b).strip
+ # don't allow more than 2 line breaks
+ .replace(re"\n{2,}", "\n\n")
+ # replace mnemonics
+ .replace("&lt;", "<")
+ .replace("&gt;", ">")
+ .replace("&quot;", "\"")
+ .replace("&amp;", "&")
+
+ proc findAllMatches(start: int, found: seq[AurComment]): seq[AurComment] =
+ var matches: array[3, string]
+ let index = content.find(commentRe, matches, start)
+ if index >= 0:
+ findAllMatches(index + 1, found & (matches[0].strip, matches[1].strip,
+ transformComment(matches[2])))
+ else:
+ found
+
+ (findAllMatches(0, @[]), none(string))
diff --git a/src/common.nim b/src/common.nim
new file mode 100644
index 0000000..e17c802
--- /dev/null
+++ b/src/common.nim
@@ -0,0 +1,392 @@
+import
+ future, options, os, osproc, posix, sequtils, sets, strutils, tables,
+ args, config, package, pacman, utils,
+ "wrapper/alpm"
+
+type
+ SyncFoundPackageInfo* = tuple[
+ base: string,
+ version: string,
+ arch: Option[string]
+ ]
+
+ SyncFoundInfo* = tuple[
+ repo: string,
+ pkg: Option[SyncFoundPackageInfo]
+ ]
+
+ PackageTarget* = object of RootObj
+ name*: string
+ repo*: Option[string]
+
+ SyncPackageTarget* = object of PackageTarget
+ foundInfo*: Option[SyncFoundInfo]
+
+ FullPackageTarget*[T] = object of SyncPackageTarget
+ pkgInfo*: Option[T]
+
+proc toPackageReference*(dependency: ptr AlpmDependency): PackageReference =
+ let op = case dependency.depmod:
+ of AlpmDepMod.eq: some(ConstraintOperation.eq)
+ of AlpmDepMod.ge: some(ConstraintOperation.ge)
+ of AlpmDepMod.le: some(ConstraintOperation.le)
+ of AlpmDepMod.gt: some(ConstraintOperation.gt)
+ of AlpmDepMod.lt: some(ConstraintOperation.lt)
+ else: none(ConstraintOperation)
+
+ let description = if dependency.desc != nil: some($dependency.desc) else: none(string)
+ ($dependency.name, description, op.map(o => (o, $dependency.version)))
+
+proc checkConstraints(lop: ConstraintOperation, rop: ConstraintOperation, cmp: int): bool =
+ let (x1, x2) = if cmp > 0:
+ (1, -1)
+ elif cmp < 0:
+ (-1, 1)
+ else:
+ (0, 0)
+
+ proc c(op: ConstraintOperation, x1: int, x2: int): bool =
+ case op:
+ of ConstraintOperation.eq: x1 == x2
+ of ConstraintOperation.ge: x1 >= x2
+ of ConstraintOperation.le: x1 <= x2
+ of ConstraintOperation.gt: x1 > x2
+ of ConstraintOperation.lt: x1 < x2
+
+ template a(x: int): bool = lop.c(x, x1) and rop.c(x, x2)
+
+ a(2) or a(1) or a(0) or a(-1) or a(-2)
+
+proc isProvidedBy*(package: PackageReference, by: PackageReference): bool =
+ if package.name == by.name:
+ if package.constraint.isNone or by.constraint.isNone:
+ true
+ else:
+ let lcon = package.constraint.unsafeGet
+ let rcon = package.constraint.unsafeGet
+ let cmp = vercmp(lcon.version, rcon.version)
+ checkConstraints(lcon.operation, rcon.operation, cmp)
+ else:
+ false
+
+proc checkAndRefresh*(color: bool, args: seq[Argument]): tuple[code: int, args: seq[Argument]] =
+ let refreshCount = args.count((some("y"), "refresh"))
+ if refreshCount > 0:
+ let code = pacmanRun(true, color, args
+ .keepOnlyOptions(commonOptions, upgradeCommonOptions) &
+ ("S", none(string), ArgumentType.short) &
+ ("y", none(string), ArgumentType.short).repeat(refreshCount))
+
+ let callArgs = args
+ .filter(arg => not arg.matchOption((some("y"), "refresh")))
+ (code, callArgs)
+ else:
+ (0, args)
+
+proc packageTargets*(args: seq[Argument]): seq[PackageTarget] =
+ args.targets.map(proc (target: string): PackageTarget =
+ let splitTarget = target.split('/', 2)
+ if splitTarget.len == 2:
+ PackageTarget(name: splitTarget[1], repo: some(splitTarget[0]))
+ else:
+ PackageTarget(name: target, repo: none(string)))
+
+proc isAurTargetSync*(target: SyncPackageTarget): bool =
+ target.foundInfo.isNone and (target.repo.isNone or target.repo == some("aur"))
+
+proc isAurTargetFull*[T: RpcPackageInfo](target: FullPackageTarget[T]): bool =
+ target.foundInfo.isSome and target.foundInfo.unsafeGet.repo == "aur"
+
+proc findSyncTargets*(handle: ptr AlpmHandle, dbs: seq[ptr AlpmDatabase],
+ targets: seq[PackageTarget], allowGroups: bool, checkProvides: bool):
+ (seq[SyncPackageTarget], seq[string]) =
+ let dbTable = dbs.map(d => ($d.name, d)).toTable
+
+ proc checkProvided(name: string, db: ptr AlpmDatabase): bool =
+ for pkg in db.packages:
+ for provides in pkg.provides:
+ if $provides.name == name:
+ return true
+ return false
+
+ proc findSync(target: PackageTarget): Option[SyncFoundInfo] =
+ if target.repo.isSome:
+ let repo = target.repo.unsafeGet
+
+ if dbTable.hasKey(repo):
+ let db = dbTable[repo]
+ let pkg = db[target.name]
+
+ if pkg != nil:
+ let base = if pkg.base == nil: target.name else: $pkg.base
+ return some((repo, some((base, $pkg.version, some($pkg.arch)))))
+ elif checkProvides and target.name.checkProvided(db):
+ return some((repo, none(SyncFoundPackageInfo)))
+ else:
+ return none(SyncFoundInfo)
+ else:
+ return none(SyncFoundInfo)
+ else:
+ let directResult = dbs
+ .map(db => (block:
+ let pkg = db[target.name]
+ if pkg != nil:
+ let base = if pkg.base == nil: target.name else: $pkg.base
+ some(($db.name, some((base, $pkg.version, some($pkg.arch)))))
+ else:
+ none(SyncFoundInfo)))
+ .filter(i => i.isSome)
+ .optFirst
+ .flatten
+
+ if directResult.isSome:
+ return directResult
+ else:
+ if allowGroups:
+ let groupRepo = lc[d | (d <- dbs, g <- d.groups, $g.name == target.name),
+ ptr AlpmDatabase].optFirst
+ if groupRepo.isSome:
+ return groupRepo.map(d => ($d.name, none(SyncFoundPackageInfo)))
+
+ if checkProvides:
+ for db in dbs:
+ if target.name.checkProvided(db):
+ return some(($db.name, none(SyncFoundPackageInfo)))
+ return none(SyncFoundInfo)
+ else:
+ return none(SyncFoundInfo)
+
+ let syncTargets = targets.map(t => SyncPackageTarget(name: t.name,
+ repo: t.repo, foundInfo: findSync(t)))
+ let checkAur = syncTargets.filter(isAurTargetSync).map(t => t.name)
+ (syncTargets, checkAur)
+
+proc mapAurTargets*[T: RpcPackageInfo](targets: seq[SyncPackageTarget],
+ pkgInfos: seq[T]): seq[FullPackageTarget[T]] =
+ let aurTable = pkgInfos.map(i => (i.name, i)).toTable
+
+ targets.map(proc (target: SyncPackageTarget): FullPackageTarget[T] =
+ if target.foundInfo.isNone and aurTable.hasKey(target.name):
+ let pkgInfo = aurTable[target.name]
+ let syncInfo = ("aur", some((pkgInfo.base, pkgInfo.version, none(string))))
+ FullPackageTarget[T](name: target.name, repo: target.repo,
+ foundInfo: some(syncInfo), pkgInfo: some(pkgInfo))
+ else:
+ FullPackageTarget[T](name: target.name, repo: target.repo,
+ foundInfo: target.foundInfo, pkgInfo: none(T)))
+
+proc formatArgument*(target: PackageTarget): string =
+ target.repo.map(r => r & "/" & target.name).get(target.name)
+
+proc ensureTmpOrError*(config: Config): Option[string] =
+ let tmpRootExists = try:
+ discard config.tmpRoot.existsOrCreateDir()
+ true
+ except:
+ false
+
+ if not tmpRootExists:
+ some(tr"failed to create tmp directory '$#'" % [config.tmpRoot])
+ else:
+ none(string)
+
+proc bisectVersion(repoPath: string, debug: bool, firstCommit: Option[string],
+ compareMethod: string, relativePath: string, version: string): Option[string] =
+ template forkExecWithoutOutput(args: varargs[string]): int =
+ forkWait(() => (block:
+ discard close(0)
+ if not debug:
+ discard close(1)
+ discard close(2)
+
+ execResult(args)))
+
+ let (workFirstCommit, checkFirst) = if firstCommit.isSome:
+ (firstCommit, false)
+ else:
+ (runProgram(gitCmd, "-C", repoPath,
+ "rev-list", "--max-parents=0", "@").optLast, true)
+ let realLastThreeCommits = runProgram(gitCmd, "-C", repoPath,
+ "rev-list", "--max-count=3", "@")
+ let index = workFirstCommit.map(c => realLastThreeCommits.find(c)).get(-1)
+ let lastThreeCommits = if index >= 0:
+ realLastThreeCommits[0 .. index]
+ else:
+ realLastThreeCommits
+
+ proc checkCommit(commit: string): Option[string] =
+ let checkout1Code = forkExecWithoutOutput(gitCmd, "-C", repoPath,
+ "checkout", commit)
+
+ if checkout1Code != 0:
+ none(string)
+ else:
+ let foundVersion = runProgram(pkgLibDir & "/bisect",
+ compareMethod, repoPath & "/" & relativePath, version).optFirst
+ let checkout2Code = forkExecWithoutOutput(gitCmd, "-C", repoPath,
+ "checkout", lastThreeCommits[0])
+
+ if checkout2Code != 0:
+ none(string)
+ elif foundVersion == some(version):
+ some(commit)
+ else:
+ none(string)
+
+ if lastThreeCommits.len == 0:
+ none(string)
+ elif lastThreeCommits.len == 1:
+ if checkFirst:
+ checkCommit(lastThreeCommits[0])
+ else:
+ none(string)
+ elif lastThreeCommits.len == 2:
+ let checkedCommit = checkCommit(lastThreeCommits[0])
+ if checkedCommit.isSome:
+ checkedCommit
+ elif checkFirst:
+ checkCommit(lastThreeCommits[1])
+ else:
+ none(string)
+ else:
+ # find the commit with specific package version using git bisect
+ let bisectStartCode = forkExecWithoutOutput(gitCmd, "-C", repoPath,
+ "bisect", "start", "@", workFirstCommit.get(""))
+
+ if bisectStartCode != 0:
+ none(string)
+ else:
+ discard forkExecWithoutOutput(gitCmd, "-C", repoPath,
+ "bisect", "run", pkgLibDir & "/bisect", compareMethod, relativePath, version)
+
+ let commit = runProgram(gitCmd, "-C", repoPath,
+ "rev-list", "--max-count=1", "refs/bisect/bad").optFirst
+
+ discard forkExecWithoutOutput(gitCmd, "-C", repoPath,
+ "bisect", "reset")
+
+ if commit.isSome:
+ let checkedCommit = commit.map(checkCommit).flatten
+ if checkedCommit.isSome:
+ checkedCommit
+ else:
+ # non-incremental git history (e.g. downgrade without epoch change), bisect again
+ bisectVersion(repoPath, debug, commit, compareMethod, relativePath, version)
+ elif checkFirst and workFirstCommit.isSome:
+ checkCommit(workFirstCommit.unsafeGet)
+ else:
+ none(string)
+
+proc obtainBuildPkgInfos*(config: Config,
+ pacmanTargets: seq[FullPackageTarget[RpcPackageInfo]]): (seq[PackageInfo], seq[string]) =
+ type
+ LookupBaseGroup = tuple[
+ base: string,
+ version: string,
+ arch: string,
+ repo: string
+ ]
+
+ LookupGitResult = tuple[
+ group: LookupBaseGroup,
+ git: Option[GitRepo]
+ ]
+
+ let bases: seq[LookupBaseGroup] = pacmanTargets
+ .map(target => (block:
+ let info = target.foundInfo.get
+ let pkg = info.pkg.get
+ (pkg.base, pkg.version, pkg.arch.get, info.repo)))
+ .deduplicate
+
+ let lookupResults: seq[LookupGitResult] = bases
+ .map(b => (b, lookupGitRepo(b.repo, b.base, b.arch)))
+ let notFoundRepos = lookupResults.filter(r => r.git.isNone)
+
+ if notFoundRepos.len > 0:
+ let messages = notFoundRepos.map(r => tr"$#: repository not found" % [r.group.base])
+ (newSeq[PackageInfo](), messages)
+ else:
+ let message = ensureTmpOrError(config)
+ if message.isSome:
+ (@[], @[message.unsafeGet])
+ else:
+ proc findCommitAndGetSrcInfo(base: string, version: string,
+ repo: string, git: GitRepo): seq[PackageInfo] =
+ let repoPath = repoPath(config.tmpRoot, base)
+ removeDirQuiet(repoPath)
+
+ try:
+ if forkWait(() => execResult(gitCmd, "-C", config.tmpRoot,
+ "clone", "-q", git.url, "-b", git.branch,
+ "--single-branch", base)) == 0:
+ let commit = bisectVersion(repoPath, config.debug, none(string),
+ "source", git.path, version)
+
+ if commit.isNone:
+ @[]
+ else:
+ discard forkWait(() => execResult(gitCmd, "-C", repoPath,
+ "checkout", "-q", commit.unsafeGet))
+ let output = execProcess(bashCmd, ["-c",
+ """cd "$2/$3" && "$1" --printsrcinfo""",
+ "bash", makePkgCmd, repoPath, git.path], options = {})
+ parseSrcInfo(repo, output, git.url, some(git.branch), commit, some(git.path))
+ .filter(i => i.version == version)
+ else:
+ @[]
+ finally:
+ removeDirQuiet(repoPath)
+
+ let pkgInfos = lc[x | (r <- lookupResults, x <- findCommitAndGetSrcInfo(r.group.base,
+ r.group.version, r.group.repo, r.git.get)), PackageInfo]
+
+ let pkgInfosTable = pkgInfos.map(i => (i.name, i)).toTable
+
+ let foundPkgInfos = lc[x | (y <- pacmanTargets,
+ x <- pkgInfosTable.opt(y.name)), PackageInfo]
+ let messages = pacmanTargets
+ .filter(t => not pkgInfosTable.hasKey(t.name))
+ .map(t => tr"$#: failed to get package info" % [t.name])
+
+ discard rmdir(config.tmpRoot)
+ (foundPkgInfos, messages)
+
+proc cloneRepo*(config: Config, basePackages: seq[PackageInfo]): (int, Option[string]) =
+ let base = basePackages[0].base
+ let repoPath = repoPath(config.tmpRoot, base)
+
+ let message = ensureTmpOrError(config)
+ if message.isSome:
+ (1, message)
+ elif repoPath.existsDir():
+ (0, none(string))
+ else:
+ let gitUrl = basePackages[0].gitUrl
+ let gitBranch = basePackages[0].gitBranch
+ let gitCommit = basePackages[0].gitCommit
+ let aur = basePackages[0].repo == "aur"
+ let branch = gitBranch.get("master")
+
+ let cloneCode = forkWait(() => execResult(gitCmd, "-C", config.tmpRoot,
+ "clone", "-q", gitUrl, "-b", branch, "--single-branch", base))
+
+ if cloneCode == 0:
+ if gitCommit.isSome:
+ let code = forkWait(() => execResult(gitCmd, "-C", repoPath,
+ "reset", "-q", "--hard", gitCommit.unsafeGet))
+ (code, none(string))
+ elif aur: (block:
+ let commit = bisectVersion(repoPath, config.debug, none(string),
+ "srcinfo", basePackages[0].gitPath.get("."), basePackages[0].version)
+
+ if commit.isSome:
+ let code = forkWait(() => execResult(gitCmd, "-C", repoPath,
+ "reset", "-q", "--hard", commit.unsafeGet))
+ (code, none(string))
+ else:
+ (1, none(string)))
+ else:
+ (1, none(string))
+ else:
+ (cloneCode, none(string))
diff --git a/src/config.nim b/src/config.nim
new file mode 100644
index 0000000..abcf376
--- /dev/null
+++ b/src/config.nim
@@ -0,0 +1,125 @@
+import
+ future, options, posix, re, sets, strutils, tables,
+ utils
+
+type
+ ColorMode* {.pure.} = enum
+ colorNever = "never",
+ colorAuto = "auto",
+ colorAlways = "always"
+
+ CommonConfig* = object of RootObj
+ dbs*: seq[string]
+ arch*: string
+ debug*: bool
+ progressBar*: bool
+ verbosePkgList*: bool
+ ignorePkgs*: HashSet[string]
+ ignoreGroups*: HashSet[string]
+
+ PacmanConfig* = object of CommonConfig
+ rootOption*: Option[string]
+ dbOption*: Option[string]
+ colorMode*: ColorMode
+
+ Config* = object of CommonConfig
+ root*: string
+ db*: string
+ tmpRoot*: string
+ color*: bool
+ aurComments*: bool
+ checkIgnored*: bool
+ printAurNotFound*: bool
+ viewNoDefault*: bool
+
+proc readConfigFile*(configFile: string):
+ (OrderedTable[string, ref Table[string, string]], bool) =
+ var file: File
+ var table = initOrderedTable[string, ref Table[string, string]]()
+ var category: ref Table[string, string]
+ var currentCategory = ""
+
+ let wasError = if file.open(configFile):
+ try:
+ var matches: array[2, string]
+
+ while true:
+ let rawLine = readLine(file).strip(leading = false, trailing = true)
+ let commentIndex = rawLine.find('#')
+ let line = if commentIndex >= 0:
+ rawLine[0 .. commentIndex - 1].strip(leading = false, trailing = true)
+ else:
+ rawLine
+
+ if line.len > 0:
+ if line.match(re"\[(.*)\]", matches):
+ currentCategory = matches[0]
+ if table.hasKey(currentCategory):
+ category = table[currentCategory]
+ else:
+ category = newTable[string, string]()
+ table[currentCategory] = category
+ elif currentCategory.len > 0:
+ if line.match(re"\ *(\w+)\ *=\ *(.*)", matches):
+ category[].add(matches[0], matches[1])
+ else:
+ category[].add(line.strip(leading = true, trailing = false), "")
+
+ false
+ except EOFError:
+ false
+ except IOError:
+ true
+ finally:
+ file.close()
+ else:
+ true
+
+ (table, wasError)
+
+proc ignored*(config: Config, name: string, groups: openArray[string]): bool =
+ name in config.ignorePkgs or (config.ignoreGroups * groups.toSet).len > 0
+
+proc isRootDefault*(config: Config): bool =
+ config.root == "/"
+
+proc get*(colorMode: ColorMode): bool =
+ case colorMode:
+ of ColorMode.colorNever: false
+ of ColorMode.colorAlways: true
+ of ColorMode.colorAuto: isatty(1) == 1
+
+proc root*(config: PacmanConfig): string =
+ config.rootOption.get("/")
+
+proc db*(config: PacmanConfig): string =
+ if config.dbOption.isSome:
+ config.dbOption.unsafeGet
+ else:
+ let root = config.root
+ let workRoot = if root == "/": "" else: root
+ workRoot & localStateDir & "/lib/pacman/"
+
+proc obtainConfig*(config: PacmanConfig): Config =
+ let (configTable, _) = readConfigFile(sysConfDir & "/pakku.conf")
+ let options = configTable.opt("options").map(t => t[]).get(initTable[string, string]())
+
+ let root = config.root
+ let db = config.db
+ let color = config.colorMode.get
+
+ let (userId, userName) = getUser()
+ let tmpRoot = options.opt("TmpDir").get("/tmp/pakku-${USER}")
+ .replace("${UID}", $userId)
+ .replace("${USER}", userName)
+ let aurComments = options.hasKey("AurComments")
+ let checkIgnored = options.hasKey("CheckIgnored")
+ let printAurNotFound = options.hasKey("PrintAurNotFound")
+ let viewNoDefault = options.hasKey("ViewNoDefault")
+
+ Config(root: root, db: db, tmpRoot: tmpRoot, color: color,
+ dbs: config.dbs, arch: config.arch, debug: config.debug,
+ progressBar: config.progressBar, verbosePkgList: config.verbosePkgList,
+ ignorePkgs: config.ignoreGroups, ignoreGroups: config.ignoreGroups,
+ aurComments: aurComments, checkIgnored: checkIgnored,
+ printAurNotFound: printAurNotFound, viewNoDefault: viewNoDefault)
diff --git a/src/feature/localquery.nim b/src/feature/localquery.nim
new file mode 100644
index 0000000..b07ec98
--- /dev/null
+++ b/src/feature/localquery.nim
@@ -0,0 +1,81 @@
+import
+ algorithm, future, options, sequtils, sets, strutils, tables,
+ "../args", "../common", "../config", "../format", "../package", "../pacman", "../utils",
+ "../wrapper/alpm"
+
+proc handleQueryOrphans*(args: seq[Argument], config: Config): int =
+ type Package = tuple[name: string, explicit: bool]
+
+ let (installed, alternatives) = withAlpm(config.root, config.db,
+ newSeq[string](), config.arch, handle, dbs, errors):
+ for e in errors: printError(config.color, e)
+
+ var installed = initTable[Package, HashSet[PackageReference]]()
+ var alternatives = initTable[string, HashSet[PackageReference]]()
+
+ for pkg in handle.local.packages:
+ proc fixProvides(reference: PackageReference): PackageReference =
+ if reference.constraint.isNone:
+ (reference.name, reference.description,
+ some((ConstraintOperation.eq, $pkg.version)))
+ else:
+ reference
+
+ let depends = toSeq(pkg.depends.items)
+ .map(toPackageReference).toSet
+ let optional = toSeq(pkg.optional.items)
+ .map(toPackageReference).toSet
+ let provides = toSeq(pkg.provides.items)
+ .map(toPackageReference).map(fixProvides).toSet
+
+ installed.add(($pkg.name, pkg.reason == AlpmReason.explicit),
+ depends + optional)
+ if provides.len > 0:
+ alternatives.add($pkg.name, provides)
+
+ (installed, alternatives)
+
+ let providedBy = lc[(y, x.key) | (x <- alternatives.namedPairs, y <- x.value),
+ tuple[reference: PackageReference, name: string]]
+
+ let installedSeq = lc[x | (x <- installed.pairs),
+ tuple[package: Package, dependencies: HashSet[PackageReference]]]
+ let explicit = installedSeq
+ .filter(t => t.package.explicit)
+ .map(t => t.package.name)
+ .toSet
+
+ proc findRequired(results: HashSet[string], check: HashSet[string]): HashSet[string] =
+ let full = results + check
+
+ let direct = lc[x | (y <- installedSeq, y.package.name in check,
+ x <- y.dependencies), PackageReference]
+
+ let indirect = lc[x.name | (y <- direct, x <- providedBy,
+ y.isProvidedBy(x.reference)), string].toSet
+
+ let checkNext = (direct.map(p => p.name).toSet + indirect) - full
+ if checkNext.len > 0: findRequired(full, checkNext) else: full
+
+ let required = findRequired(initSet[string](), explicit)
+ let orphans = installedSeq.map(t => t.package.name).toSet - required
+
+ let targets = args.targets.map(t => (if t[0 .. 5] == "local/": t[6 .. ^1] else: t))
+
+ # Provide similar output for not installed packages
+ let unknownTargets = targets.toSet - toSeq(installed.keys).map(p => p.name).toSet
+ let results = if targets.len > 0:
+ targets.filter(t => t in orphans or t in unknownTargets)
+ else:
+ toSeq(orphans.items).sorted(cmp)
+
+ if results.len > 0:
+ let newArgs = args.filter(arg => not arg.isTarget and
+ not arg.matchOption((some("t"), "unrequired")) and
+ not arg.matchOption((some("d"), "deps"))) &
+ results.map(r => (r, none(string), ArgumentType.target))
+ pacmanExec(false, config.color, newArgs)
+ elif targets.len == 0:
+ 0
+ else:
+ 1
diff --git a/src/feature/syncinfo.nim b/src/feature/syncinfo.nim
new file mode 100644
index 0000000..3c64282
--- /dev/null
+++ b/src/feature/syncinfo.nim
@@ -0,0 +1,130 @@
+import
+ future, options, posix, sequtils, strutils, times,
+ "../args", "../aur", "../common", "../config", "../format", "../package",
+ "../pacman", "../utils",
+ "../wrapper/alpm"
+
+const
+ pacmanInfoStrings = [
+ "Architecture",
+ "Backup Files",
+ "Build Date",
+ "Compressed Size",
+ "Conflicts With",
+ "Depends On",
+ "Description",
+ "Download Size",
+ "Groups",
+ "Install Date",
+ "Install Reason",
+ "Install Script",
+ "Installed Size",
+ "Licenses",
+ "MD5 Sum",
+ "Name",
+ "Optional Deps",
+ "Optional For",
+ "Packager",
+ "Provides",
+ "Replaces",
+ "Repository",
+ "Required By",
+ "SHA-256 Sum",
+ "Signatures",
+ "URL",
+ "Validated By",
+ "Version"
+ ]
+
+proc formatDeps(title: string, config: Config,
+ refs: seq[ArchPackageReference]): PackageLineFormat =
+ proc formatDep(reference: ArchPackageReference): (string, bool) =
+ reference.reference.description
+ .map(d => ($reference.reference & ": " & d, true))
+ .get(($reference.reference, false))
+
+ let values: seq[tuple[title: string, hasDesc: bool]] = refs
+ .filter(r => r.arch.isNone or r.arch == some(config.arch))
+ .map(formatDep)
+
+ if values.len > 0:
+ (title, values.map(v => v.title), values.map(v => v.hasDesc).foldl(a or b))
+ else:
+ (title, @[], false)
+
+proc formatDate(date: Option[int64]): seq[string] =
+ if date.isSome:
+ var time = (posix.Time) date.unsafeGet
+ var ltime: Tm
+ discard localtime_r(time, ltime)
+ var buffer: array[100, char]
+ let res = strftime(addr(buffer), buffer.len, "%c", ltime)
+ if res > 0: @[buffer.toString(none(int))] else: @[]
+ else:
+ @[]
+
+proc handleTarget(config: Config, padding: int, args: seq[Argument],
+ target: FullPackageTarget[PackageInfo]): int =
+ if target.foundInfo.isSome:
+ if isAurTargetFull[PackageInfo](target):
+ let pkgInfo = target.pkgInfo.unsafeGet
+
+ printPackageInfo(padding, config.color,
+ (trp"Repository", @["aur"], false),
+ (trp"Name", @[pkgInfo.name], false),
+ (trp"Version", @[pkgInfo.version], false),
+ (trp"Description", toSeq(pkgInfo.description.items), false),
+ (trp"Architecture", pkgInfo.archs, false),
+ (trp"URL", toSeq(pkgInfo.url.items), false),
+ (trp"Licenses", pkgInfo.licenses, false),
+ (trp"Groups", pkgInfo.groups, false),
+ formatDeps(trp"Provides", config, pkgInfo.provides),
+ formatDeps(trp"Depends On", config, pkgInfo.depends),
+ formatDeps(trp"Optional Deps", config, pkgInfo.optional),
+ formatDeps(trp"Conflicts With", config, pkgInfo.conflicts),
+ formatDeps(trp"Replaces", config, pkgInfo.replaces),
+ (tr"Maintainer", toSeq(pkgInfo.maintainer.items()), false),
+ (tr"First Submitted", pkgInfo.firstSubmitted.formatDate, false),
+ (tr"Last Modified", pkgInfo.lastModified.formatDate, false),
+ (tr"Rating", @[formatPkgRating(pkgInfo.votes, pkgInfo.popularity)], false))
+
+ 0
+ else:
+ pacmanRun(false, config.color, args &
+ (target.formatArgument, none(string), ArgumentType.target))
+ else:
+ if target.repo == some("aur"):
+ printError(config.color, trp("package '%s' was not found\n") % [target.formatArgument])
+ 1
+ else:
+ pacmanRun(false, config.color, args &
+ (target.formatArgument, none(string), ArgumentType.target))
+
+proc handleSyncInfo*(args: seq[Argument], config: Config): int =
+ let (_, callArgs) = checkAndRefresh(config.color, args)
+ let targets = args.packageTargets
+
+ let (syncTargets, checkAur) = withAlpm(config.root, config.db,
+ config.dbs, config.arch, handle, dbs, errors):
+ for e in errors: printError(config.color, e)
+ findSyncTargets(handle, dbs, targets, false, false)
+
+ let (pkgInfos, aerrors) = getAurPackageInfo(checkAur, none(seq[RpcPackageInfo]),
+ proc (a: int, b: int) = discard)
+ for e in aerrors: printError(config.color, e)
+
+ let fullTargets = mapAurTargets[PackageInfo](syncTargets, pkgInfos)
+
+ let code = min(aerrors.len, 1)
+ if fullTargets.filter(isAurTargetFull[PackageInfo]).len == 0:
+ if code == 0:
+ pacmanExec(false, config.color, callArgs)
+ else:
+ discard pacmanRun(false, config.color, callArgs)
+ code
+ else:
+ let finalArgs = callArgs.filter(arg => not arg.isTarget)
+ let padding = pacmanInfoStrings.map(s => s.trp).computeMaxLength
+
+ let codes = code & lc[handleTarget(config, padding, finalArgs, x) | (x <- fullTargets), int]
+ codes.filter(c => c != 0).optFirst.get(0)
diff --git a/src/feature/syncinstall.nim b/src/feature/syncinstall.nim
new file mode 100644
index 0000000..5f6e082
--- /dev/null
+++ b/src/feature/syncinstall.nim
@@ -0,0 +1,827 @@
+import
+ algorithm, future, options, os, posix, sequtils, sets, strutils, tables,
+ "../args", "../aur", "../config", "../common", "../format", "../package",
+ "../pacman", "../utils",
+ "../wrapper/alpm"
+
+type
+ Installed = tuple[
+ name: string,
+ version: string,
+ groups: seq[string],
+ foreign: bool
+ ]
+
+ SatisfyResult = tuple[
+ installed: bool,
+ name: string,
+ buildPkgInfo: Option[PackageInfo]
+ ]
+
+ BuildResult = tuple[
+ version: string,
+ arch: string,
+ ext: string,
+ names: seq[string]
+ ]
+
+proc groupsSeq(pkg: ptr AlpmPackage): seq[string] =
+ toSeq(pkg.groups.items).map(s => $s)
+
+proc orderInstallation(ordered: seq[seq[seq[PackageInfo]]], grouped: seq[seq[PackageInfo]],
+ dependencies: Table[PackageReference, SatisfyResult]): seq[seq[seq[PackageInfo]]] =
+ let orderedNamesSet = lc[c.name | (a <- ordered, b <- a, c <- b), string].toSet
+
+ proc hasBuildDependency(pkgInfos: seq[PackageInfo]): bool =
+ for pkgInfo in pkgInfos:
+ for reference in pkgInfo.allDepends:
+ let satres = dependencies[reference.reference]
+ if satres.buildPkgInfo.isSome and
+ not (satres.buildPkgInfo.unsafeGet in pkgInfos) and
+ not (satres.buildPkgInfo.unsafeGet.name in orderedNamesSet):
+ return true
+ return false
+
+ let split: seq[tuple[pkgInfos: seq[PackageInfo], dependent: bool]] =
+ grouped.map(i => (i, i.hasBuildDependency))
+
+ let newOrdered = ordered & split.filter(s => not s.dependent).map(s => s.pkgInfos)
+ let unordered = split.filter(s => s.dependent).map(s => s.pkgInfos)
+
+ if unordered.len > 0:
+ if unordered.len == grouped.len:
+ newOrdered & unordered
+ else:
+ orderInstallation(newOrdered, unordered, dependencies)
+ else:
+ newOrdered
+
+proc orderInstallation(pkgInfos: seq[PackageInfo],
+ dependencies: Table[PackageReference, SatisfyResult]): seq[seq[seq[PackageInfo]]] =
+ let grouped = pkgInfos.groupBy(i => i.base).map(p => p.values)
+
+ orderInstallation(@[], grouped, dependencies)
+ .map(x => x.filter(s => s.len > 0))
+ .filter(x => x.len > 0)
+
+proc findDependencies(config: Config, handle: ptr AlpmHandle, dbs: seq[ptr AlpmDatabase],
+ satisfied: Table[PackageReference, SatisfyResult], unsatisfied: seq[PackageReference],
+ printMode: bool, noaur: bool): (Table[PackageReference, SatisfyResult], seq[PackageReference]) =
+ proc findInSatisfied(reference: PackageReference): Option[PackageInfo] =
+ for satref, res in satisfied.pairs:
+ if res.buildPkgInfo.isSome:
+ let pkgInfo = res.buildPkgInfo.unsafeGet
+ if satref == reference or reference.isProvidedBy((pkgInfo.name, none(string),
+ some((ConstraintOperation.eq, pkgInfo.version)))):
+ return some(pkgInfo)
+ for provides in pkgInfo.provides:
+ if provides.arch.isNone or provides.arch == some(config.arch):
+ if reference.isProvidedBy(provides.reference):
+ return some(pkgInfo)
+ return none(PackageInfo)
+
+ proc findInDatabaseWithGroups(db: ptr AlpmDatabase, reference: PackageReference,
+ directName: bool): Option[tuple[name: string, groups: seq[string]]] =
+ for pkg in db.packages:
+ if reference.isProvidedBy(($pkg.name, none(string),
+ some((ConstraintOperation.eq, $pkg.version)))):
+ return some(($pkg.name, pkg.groupsSeq))
+ for provides in pkg.provides:
+ if reference.isProvidedBy(provides.toPackageReference):
+ if directName:
+ return some(($pkg.name, pkg.groupsSeq))
+ else:
+ return some(($provides.name, pkg.groupsSeq))
+ return none((string, seq[string]))
+
+ proc findInDatabase(db: ptr AlpmDatabase, reference: PackageReference,
+ directName: bool, checkIgnored: bool): Option[string] =
+ let res = findInDatabaseWithGroups(db, reference, directName)
+ if res.isSome:
+ let r = res.unsafeGet
+ if checkIgnored and config.ignored(r.name, r.groups):
+ none(string)
+ else:
+ some(r.name)
+ else:
+ none(string)
+
+ proc findInDatabases(reference: PackageReference,
+ directName: bool, checkIgnored: bool): Option[string] =
+ for db in dbs:
+ let name = findInDatabase(db, reference, directName, checkIgnored)
+ if name.isSome:
+ return name
+ return none(string)
+
+ proc find(reference: PackageReference): Option[SatisfyResult] =
+ let localName = findInDatabase(handle.local, reference, true, false)
+ if localName.isSome:
+ some((true, localName.unsafeGet, none(PackageInfo)))
+ else:
+ let pkgInfo = findInSatisfied(reference)
+ if pkgInfo.isSome:
+ some((false, pkgInfo.unsafeGet.name, pkgInfo))
+ else:
+ let syncName = findInDatabases(reference, false, true)
+ if syncName.isSome:
+ some((false, syncName.unsafeGet, none(PackageInfo)))
+ else:
+ none(SatisfyResult)
+
+ type ReferenceResult = tuple[reference: PackageReference, result: Option[SatisfyResult]]
+
+ let findResult: seq[ReferenceResult] = unsatisfied.map(r => (r, r.find))
+ let success = findResult.filter(r => r.result.isSome)
+ let aurCheck = findResult.filter(r => r.result.isNone).map(r => r.reference)
+
+ let (aurSuccess, aurFail) = if not noaur and aurCheck.len > 0: (block:
+ let (update, terminate) = if aurCheck.len >= 4:
+ printProgressShare(config.progressBar, tr"checking build dependencies")
+ else:
+ (proc (a: int, b: int) {.closure.} = discard, proc {.closure.} = discard)
+ try:
+ withAur():
+ let (pkgInfos, aerrors) = getAurPackageInfo(aurCheck.map(r => r.name),
+ none(seq[RpcPackageInfo]), update)
+ for e in aerrors: printError(config.color, e)
+
+ let acceptedPkgInfos = pkgInfos.filter(i => not config.ignored(i.name, i.groups))
+ let aurTable = acceptedPkgInfos.map(i => (i.name, i)).toTable
+ let aurResult = aurCheck.map(proc (reference: PackageReference): ReferenceResult =
+ if aurTable.hasKey(reference.name):
+ (reference, some((false, reference.name, some(aurTable[reference.name]))))
+ else:
+ (reference, none(SatisfyResult)))
+
+ let aurSuccess = aurResult.filter(r => r.result.isSome)
+ let aurFail = aurResult.filter(r => r.result.isNone).map(r => r.reference)
+ (aurSuccess, aurFail)
+ finally:
+ terminate())
+ else:
+ (@[], aurCheck)
+
+ let newSatisfied = (toSeq(satisfied.pairs) &
+ success.map(r => (r.reference, r.result.unsafeGet)) &
+ aurSuccess.map(r => (r.reference, r.result.unsafeGet))).toTable
+
+ let newUnsatisfied = lc[x.reference | (y <- aurSuccess,
+ r <- y.result, i <- r.buildPkgInfo, x <- i.allDepends,
+ x.arch.isNone or x.arch == some(config.arch)), PackageReference].deduplicate
+
+ if aurFail.len > 0:
+ (newSatisfied, aurFail)
+ elif newUnsatisfied.len > 0:
+ findDependencies(config, handle, dbs, newSatisfied, newUnsatisfied, printMode, noaur)
+ else:
+ (newSatisfied, @[])
+
+proc findDependencies(config: Config, handle: ptr AlpmHandle,
+ dbs: seq[ptr AlpmDatabase], pkgInfos: seq[PackageInfo], printMode: bool, noaur: bool):
+ (Table[PackageReference, SatisfyResult], seq[PackageReference]) =
+ let satisfied = pkgInfos.map(p => ((p.name, none(string), none(VersionConstraint)),
+ (false, p.name, some(p)))).toTable
+ let unsatisfied = lc[x.reference | (i <- pkgInfos, x <- i.allDepends,
+ x.arch.isNone or x.arch == some(config.arch)), PackageReference].deduplicate
+ findDependencies(config, handle, dbs, satisfied, unsatisfied, printMode, noaur)
+
+proc filterNotFoundSyncTargets[T: RpcPackageInfo](syncTargets: seq[SyncPackageTarget],
+ pkgInfos: seq[T]): (Table[string, T], seq[SyncPackageTarget]) =
+ let rpcInfoTable = pkgInfos.map(d => (d.name, d)).toTable
+
+ proc notFoundOrFoundInAur(target: SyncPackageTarget): bool =
+ target.foundInfo.isNone and
+ not (target.isAurTargetSync and rpcInfoTable.hasKey(target.name))
+
+ # collect packages which were found neither in sync DB nor in AUR
+ let notFoundTargets = syncTargets.filter(notFoundOrFoundInAur)
+
+ (rpcInfoTable, notFoundTargets)
+
+proc printSyncNotFound(config: Config, notFoundTargets: seq[SyncPackageTarget]) =
+ let dbs = config.dbs.toSet
+
+ for target in notFoundTargets:
+ if target.repo.isNone or target.repo == some("aur") or target.repo.unsafeGet in dbs:
+ printError(config.color, trp("target not found: %s\n") % [target.name])
+ else:
+ printError(config.color, trp("database not found: %s\n") % [target.repo.unsafeGet])
+
+proc printUnsatisfied(config: Config,
+ satisfied: Table[PackageReference, SatisfyResult], unsatisfied: seq[PackageReference]) =
+ if unsatisfied.len > 0:
+ for _, satres in satisfied.pairs:
+ for pkgInfo in satres.buildPkgInfo:
+ for reference in pkgInfo.allDepends:
+ let pref = reference.reference
+ if pref in unsatisfied:
+ printError(config.color,
+ trp("unable to satisfy dependency '%s' required by %s\n") %
+ [$pref, pkgInfo.name])
+
+proc editLoop(config: Config, base: string, repoPath: string, gitPath: Option[string],
+ defaultYes: bool, noconfirm: bool): char =
+ proc editFileLoop(file: string): char =
+ let default = if defaultYes: 'y' else: 'n'
+ let res = printColonUserChoice(config.color,
+ tr"View and edit $#?" % [base & "/" & file], ['y', 'n', 's', 'a', '?'],
+ default, '?', noconfirm, 'n')
+
+ if res == '?':
+ printUserInputHelp(('s', tr"skip all files"),
+ ('a', tr"abort operation"))
+ editFileLoop(file)
+ elif res == 'y':
+ let visualEnv = getenv("VISUAL")
+ let editorEnv = getenv("EDITOR")
+ let editor = if visualEnv != nil and visualEnv.len > 0:
+ $visualEnv
+ elif editorEnv != nil and editorEnv.len > 0:
+ $editorEnv
+ else:
+ printColonUserInput(config.color, tr"Enter editor executable name" & ":",
+ noconfirm, "", "")
+
+ if editor.strip.len == 0:
+ 'n'
+ else:
+ discard forkWait(proc: int =
+ discard chdir(buildPath(repoPath, gitPath))
+ execResult(bashCmd, "-c", """$1 "$2"""", "bash", editor, file))
+ editFileLoop(file)
+ else:
+ res
+
+ let rawFiles = if gitPath.isSome:
+ runProgram(gitCmd, "-C", repoPath, "ls-tree", "-r", "--name-only", "@",
+ gitPath.unsafeGet & "/").map(s => s[gitPath.unsafeGet.len + 1 .. ^1])
+ else:
+ runProgram(gitCmd, "-C", repoPath, "ls-tree", "-r", "--name-only", "@")
+
+ let files = ("PKGBUILD" & rawFiles.filter(x => x != ".SRCINFO")).deduplicate
+
+ proc editFileLoopAll(index: int): char =
+ if index < files.len:
+ let res = editFileLoop(files[index])
+ if res == 'n': editFileLoopAll(index + 1) else: res
+ else:
+ 'n'
+
+ editFileLoopAll(0)
+
+proc buildLoop(config: Config, pkgInfos: seq[PackageInfo], noconfirm: bool,
+ noextract: bool): (Option[BuildResult], int) =
+ let base = pkgInfos[0].base
+ let repoPath = repoPath(config.tmpRoot, base)
+ let gitPath = pkgInfos[0].gitPath
+ let buildPath = buildPath(repoPath, gitPath)
+
+ let buildCode = forkWait(proc: int =
+ if chdir(buildPath) == 0:
+ discard setenv("PKGDEST", config.tmpRoot, 1)
+ discard setenv("CARCH", config.arch, 1)
+
+ if not noextract:
+ removeDirQuiet(buildPath & "src")
+
+ let optional: seq[tuple[arg: string, cond: bool]] = @[
+ ("-e", noextract),
+ ("-m", not config.color)
+ ]
+
+ execResult(@[makepkgCmd, "-f"] &
+ optional.filter(o => o.cond).map(o => o.arg))
+ else:
+ quit(1))
+
+ if buildCode != 0:
+ printError(config.color, tr"failed to build '$#'" % [base])
+ (none(BuildResult), buildCode)
+ else:
+ let confFileEnv = getenv("MAKEPKG_CONF")
+ let confFile = if confFileEnv == nil or confFileEnv.len == 0:
+ sysConfDir & "/makepkg.conf"
+ else:
+ $confFileEnv
+
+ let envExt = getenv("PKGEXT")
+ let confExt = if envExt == nil or envExt.len == 0:
+ runProgram(bashCmd, "-c",
+ "source \"$@\" && echo \"$PKGEXT\"",
+ "bash", confFile).optFirst.get("")
+ else:
+ $envExt
+
+ let extracted = runProgram(bashCmd, "-c",
+ """source "$@" && echo "$epoch" && echo "$pkgver" && """ &
+ """echo "$pkgrel" && echo "${arch[@]}" && echo "${pkgname[@]}"""",
+ "bash", buildPath & "/PKGBUILD")
+ if extracted.len != 5:
+ printError(config.color, tr"failed to extract package info '$#'" % [base])
+ (none(BuildResult), 1)
+ else:
+ let epoch = extracted[0]
+ let versionShort = extracted[1] & "-" & extracted[2]
+ let version = if epoch.len > 0: epoch & ":" & versionShort else: versionShort
+ let archs = extracted[3].split(" ").toSet
+ let arch = if config.arch in archs: config.arch else: "any"
+ let names = extracted[4].split(" ")
+
+ (some((version, arch, $confExt, names)), 0)
+
+proc buildFromSources(config: Config, commonArgs: seq[Argument],
+ pkgInfos: seq[PackageInfo], noconfirm: bool): (Option[BuildResult], int) =
+ let base = pkgInfos[0].base
+ let (cloneCode, cloneErrorMessage) = cloneRepo(config, pkgInfos)
+
+ if cloneCode != 0:
+ for e in cloneErrorMessage: printError(config.color, e)
+ printError(config.color, tr"$#: failed to clone git repository" % [base])
+ (none(BuildResult), cloneCode)
+ else:
+ proc loop(noextract: bool, showEditLoop: bool): (Option[BuildResult], int) =
+ let res = if showEditLoop:
+ editLoop(config, base, repoPath(config.tmpRoot, base), pkgInfos[0].gitPath,
+ false, noconfirm)
+ else:
+ 'n'
+
+ if res == 'a':
+ (none(BuildResult), 1)
+ else:
+ let (buildResult, code) = buildLoop(config, pkgInfos,
+ noconfirm, noextract)
+
+ if code != 0:
+ proc ask(): char =
+ let res = printColonUserChoice(config.color,
+ tr"Build failed, retry?", ['y', 'e', 'n', '?'], 'n', '?',
+ noconfirm, 'n')
+ if res == '?':
+ printUserInputHelp(('e', tr"retry with --noextract option"))
+ ask()
+ else:
+ res
+
+ let res = ask()
+ if res == 'e':
+ loop(true, true)
+ elif res == 'y':
+ loop(false, true)
+ else:
+ (buildResult, code)
+ else:
+ (buildResult, code)
+
+ loop(false, false)
+
+proc installGroupFromSources(config: Config, commonArgs: seq[Argument],
+ basePackages: seq[seq[PackageInfo]], explicits: HashSet[string], noconfirm: bool): int =
+ proc buildNext(index: int, buildResults: seq[BuildResult]): (seq[BuildResult], int) =
+ if index < basePackages.len:
+ let (buildResult, code) = buildFromSources(config, commonArgs,
+ basePackages[index], noconfirm)
+
+ if code != 0:
+ (buildResults, code)
+ else:
+ buildNext(index + 1, buildResults & buildResult.unsafeGet)
+ else:
+ (buildResults, 0)
+
+ let (buildResults, buildCode) = buildNext(0, @[])
+
+ proc formatArchiveFile(br: BuildResult, name: string): string =
+ config.tmpRoot & "/" & name & "-" & br.version & "-" & br.arch & br.ext
+
+ let files = lc[(name, formatArchiveFile(br, name)) |
+ (br <- buildResults, name <- br.names), (string, string)].toTable
+ let install = lc[x | (g <- basePackages, i <- g, x <- files.opt(i.name)), string]
+
+ proc handleTmpRoot(clear: bool) =
+ for _, file in files:
+ if clear or not (file in install):
+ try:
+ removeFile(file)
+ except:
+ discard
+
+ if not clear:
+ printWarning(config.color, tr"packages are saved to '$#'" % [config.tmpRoot])
+
+ if buildCode != 0:
+ handleTmpRoot(true)
+ buildCode
+ else:
+ let res = printColonUserChoice(config.color,
+ tr"Continue installing?", ['y', 'n'], 'y', 'n',
+ noconfirm, 'y')
+
+ if res != 'y':
+ handleTmpRoot(false)
+ 1
+ else:
+ let explicit = basePackages.filter(p => p.filter(i => i.name in explicits).len > 0).len > 0
+ let asdepsSeq = if not explicit: @[("asdeps", none(string), ArgumentType.long)] else: @[]
+
+ let installCode = pacmanRun(true, config.color, commonArgs &
+ ("U", none(string), ArgumentType.short) & asdepsSeq &
+ install.map(i => (i, none(string), ArgumentType.target)))
+
+ if installCode != 0:
+ handleTmpRoot(false)
+ installCode
+ else:
+ handleTmpRoot(true)
+ 0
+
+proc handleInstall(args: seq[Argument], config: Config, upgradeCount: int,
+ noconfirm: bool, explicits: HashSet[string], installed: seq[Installed],
+ dependencies: Table[PackageReference, SatisfyResult],
+ directPacmanTargets: seq[string], additionalPacmanTargets: seq[string],
+ basePackages: seq[seq[seq[PackageInfo]]]): int =
+ let (directCode, directSome) = if directPacmanTargets.len > 0 or upgradeCount > 0:
+ (pacmanRun(true, config.color, args.filter(arg => not arg.isTarget) &
+ directPacmanTargets.map(t => (t, none(string), ArgumentType.target))), true)
+ else:
+ (0, false)
+
+ if directCode != 0:
+ directCode
+ else:
+ let commonArgs = args.keepOnlyOptions(commonOptions, upgradeCommonOptions)
+
+ let (paths, confirmAndCloneCode) = if basePackages.len > 0: (block:
+ let installedVersions = installed.map(i => (i.name, i.version)).toTable
+
+ printPackages(config.color, config.verbosePkgList,
+ lc[(i.name, i.repo, installedVersions.opt(i.name), i.version) |
+ (g <- basePackages, b <- g, i <- b), PackageInstallFormat]
+ .sorted((a, b) => cmp(a.name, b.name)))
+ let input = printColonUserChoice(config.color,
+ tr"Proceed with building?", ['y', 'n'], 'y', 'n', noconfirm, 'y')
+
+ if input == 'y':
+ let (update, terminate) = if config.debug:
+ (proc (a: int, b: int) {.closure.} = discard, proc {.closure.} = discard)
+ else:
+ printProgressShare(config.progressBar, tr"cloning repositories")
+
+ let flatBasePackages = lc[x | (a <- basePackages, x <- a), seq[PackageInfo]]
+ update(0, flatBasePackages.len)
+
+ proc cloneNext(index: int, paths: seq[string]): (seq[string], int) =
+ if index < flatBasePackages.len:
+ let pkgInfos = flatBasePackages[index]
+ let base = pkgInfos[0].base
+ let repoPath = repoPath(config.tmpRoot, base)
+ let (cloneCode, cloneErrorMessage) = cloneRepo(config, flatBasePackages[index])
+
+ if cloneCode == 0:
+ update(index + 1, flatBasePackages.len)
+ cloneNext(index + 1, paths & repoPath)
+ else:
+ terminate()
+ for e in cloneErrorMessage: printError(config.color, e)
+ printError(config.color, tr"$#: failed to clone git repository" %
+ [pkgInfos[0].base])
+ (paths & repoPath, cloneCode)
+ else:
+ terminate()
+ (paths, 0)
+
+ let (paths, cloneCode) = cloneNext(0, @[])
+ if cloneCode != 0:
+ (paths, cloneCode)
+ else:
+ proc checkNext(index: int, skipEdit: bool): int =
+ if index < flatBasePackages.len:
+ let pkgInfos = flatBasePackages[index]
+ let base = pkgInfos[0].base
+ let repoPath = repoPath(config.tmpRoot, base)
+
+ let aur = pkgInfos[0].repo == "aur"
+
+ if not skipEdit and aur and config.aurComments:
+ echo(tr"downloading comments from AUR...")
+ let (comments, error) = downloadAurComments(base)
+ for e in error: printError(config.color, e)
+ if comments.len > 0:
+ let commentsReversed = toSeq(comments.reversed)
+ printComments(config.color, pkgInfos[0].maintainer, commentsReversed)
+
+ let res = if skipEdit:
+ 'n'
+ else: (block:
+ let defaultYes = aur and not config.viewNoDefault
+ editLoop(config, base, repoPath, pkgInfos[0].gitPath, defaultYes, noconfirm))
+
+ if res == 'a':
+ 1
+ else:
+ checkNext(index + 1, skipEdit or res == 's')
+ else:
+ 0
+
+ (paths, checkNext(0, false))
+ else:
+ (@[], 1))
+ else:
+ (@[], 0)
+
+ proc removeTmp() =
+ for path in paths:
+ removeDirQuiet(path)
+ discard rmdir(config.tmpRoot)
+
+ if confirmAndCloneCode != 0:
+ removeTmp()
+ confirmAndCloneCode
+ else:
+ let (additionalCode, additionalSome) = if additionalPacmanTargets.len > 0: (block:
+ printColon(config.color, tr"Installing build dependencies...")
+
+ (pacmanRun(true, config.color, commonArgs &
+ ("S", none(string), ArgumentType.short) &
+ ("needed", none(string), ArgumentType.long) &
+ ("asdeps", none(string), ArgumentType.long) &
+ additionalPacmanTargets.map(t => (t, none(string), ArgumentType.target))), true))
+ else:
+ (0, false)
+
+ if additionalCode != 0:
+ removeTmp()
+ additionalCode
+ else:
+ if basePackages.len > 0:
+ # check all pacman dependencies were installed
+ let unsatisfied = withAlpm(config.root, config.db,
+ config.dbs, config.arch, handle, dbs, errors):
+ for e in errors: printError(config.color, e)
+
+ proc checkSatisfied(reference: PackageReference): bool =
+ for pkg in handle.local.packages:
+ if reference.isProvidedBy(($pkg.name, none(string),
+ some((ConstraintOperation.eq, $pkg.version)))):
+ return true
+ for provides in pkg.provides:
+ if reference.isProvidedBy(provides.toPackageReference):
+ return true
+ return false
+
+ lc[x.key | (x <- dependencies.namedPairs, not x.value.installed and
+ x.value.buildPkgInfo.isNone and not x.key.checkSatisfied), PackageReference]
+
+ if unsatisfied.len > 0:
+ removeTmp()
+ printUnsatisfied(config, dependencies, unsatisfied)
+ 1
+ else:
+ proc installNext(index: int, lastCode: int): (int, int) =
+ if index < basePackages.len and lastCode == 0:
+ let code = installGroupFromSources(config, commonArgs,
+ basePackages[index], explicits, noconfirm)
+ installNext(index + 1, code)
+ else:
+ (lastCode, index - 1)
+
+ let (code, index) = installNext(0, 0)
+ if code != 0 and index < basePackages.len - 1:
+ printWarning(config.color, tr"installation aborted")
+ removeTmp()
+ code
+ elif not directSome and not additionalSome:
+ echo(trp(" there is nothing to do\n"))
+ 0
+ else:
+ 0
+
+proc handlePrint(args: seq[Argument], config: Config, printFormat: string,
+ upgradeCount: int, directPacmanTargets: seq[string], additionalPacmanTargets: seq[string],
+ basePackages: seq[seq[seq[PackageInfo]]]): int =
+
+ let code = if directPacmanTargets.len > 0 or
+ additionalPacmanTargets.len > 0 or upgradeCount > 0:
+ pacmanRun(false, config.color, args.filter(arg => not arg.isTarget) &
+ (directPacmanTargets & additionalPacmanTargets)
+ .map(t => (t, none(string), ArgumentType.target)))
+ else:
+ 0
+
+ if code == 0:
+ proc printWithFormat(pkgInfo: PackageInfo) =
+ echo(printFormat
+ .replace("%n", pkgInfo.name)
+ .replace("%v", pkgInfo.version)
+ .replace("%r", "aur")
+ .replace("%s", "0")
+ .replace("%l", pkgInfo.gitUrl))
+
+ for installGroup in basePackages:
+ for pkgInfos in installGroup:
+ for pkgInfo in pkgInfos:
+ printWithFormat(pkgInfo)
+ 0
+ else:
+ code
+
+proc handleSyncInstall*(args: seq[Argument], config: Config): int =
+ let (_, callArgs) = checkAndRefresh(config.color, args)
+
+ let upgradeCount = args.count((some("u"), "sysupgrade"))
+ let needed = args.check((none(string), "needed"))
+ let noaur = args.check((none(string), "noaur"))
+ let build = args.check((none(string), "build"))
+
+ let printModeArg = args.check((some("p"), "print"))
+ let printModeFormat = args.filter(arg => arg
+ .matchOption((none(string), "print-format"))).optLast
+ let printFormat = if printModeArg or printModeFormat.isSome:
+ some(printModeFormat.map(arg => arg.value.get).get("%l"))
+ else:
+ none(string)
+
+ let noconfirm = args
+ .filter(arg => arg.matchOption((none(string), "confirm")) or
+ arg.matchOption((none(string), "noconfirm"))).optLast
+ .map(arg => arg.key == "noconfirm").get(false)
+
+ let targets = args.packageTargets
+
+ let (syncTargets, checkAur, installed) = withAlpm(config.root, config.db,
+ config.dbs, config.arch, handle, dbs, errors):
+ for e in errors: printError(config.color, e)
+
+ let (syncTargets, checkAur) = findSyncTargets(handle, dbs, targets,
+ not build, not build)
+
+ let installed = lc[($p.name, $p.version, p.groupsSeq,
+ dbs.filter(d => d[p.name] != nil).len == 0) |
+ (p <- handle.local.packages), Installed]
+
+ (syncTargets, checkAur, installed)
+
+ let realCheckAur = if noaur:
+ @[]
+ elif upgradeCount > 0:
+ installed
+ .filter(i => i.foreign and
+ (config.checkIgnored or not config.ignored(i.name, i.groups)))
+ .map(i => i.name) & checkAur
+ else:
+ checkAur
+
+ withAur():
+ if realCheckAur.len > 0 and printFormat.isNone:
+ printColon(config.color, tr"Checking AUR database...")
+ let (rpcInfos, aerrors) = getRpcPackageInfo(realCheckAur)
+ for e in aerrors: printError(config.color, e)
+
+ let (rpcInfoTable, notFoundTargets) = filterNotFoundSyncTargets(syncTargets, rpcInfos)
+
+ if notFoundTargets.len > 0:
+ printSyncNotFound(config, notFoundTargets)
+ 1
+ else:
+ let fullTargets = mapAurTargets(syncTargets, rpcInfos)
+ let pacmanTargets = fullTargets.filter(t => not isAurTargetFull(t))
+ let aurTargets = fullTargets.filter(isAurTargetFull)
+
+ if upgradeCount > 0 and not noaur and printFormat.isNone and config.printAurNotFound:
+ for inst in installed:
+ if inst.foreign and not config.ignored(inst.name, inst.groups) and
+ not rpcInfoTable.hasKey(inst.name):
+ printWarning(config.color, tr"$# was not found in AUR" % [inst.name])
+
+ let installedTable = installed.map(i => (i.name, i)).toTable
+
+ proc checkNeeded(name: string, version: string): bool =
+ if installedTable.hasKey(name):
+ let i = installedTable[name]
+ vercmp(version, i.version) > 0
+ else:
+ true
+
+ let targetRpcInfos: seq[tuple[rpcInfo: RpcPackageInfo, upgradeable: bool]] =
+ aurTargets.map(t => t.pkgInfo.get).map(i => (i, checkNeeded(i.name, i.version)))
+
+ if printFormat.isNone and needed:
+ for rpcInfo in targetRpcInfos:
+ if not rpcInfo.upgradeable:
+ # not upgradeable assumes that package is installed
+ let inst = installedTable[rpcInfo.rpcInfo.name]
+ printWarning(config.color, tra("%s-%s is up to date -- skipping\n") %
+ [rpcInfo.rpcInfo.name, inst.version])
+
+ let aurTargetsSet = aurTargets.map(t => t.name).toSet
+ let fullRpcInfos = (targetRpcInfos
+ .filter(i => not needed or i.upgradeable).map(i => i.rpcInfo) &
+ rpcInfos.filter(i => upgradeCount > 0 and not (i.name in aurTargetsSet) and
+ checkNeeded(i.name, i.version))).deduplicate
+
+ if fullRpcInfos.len > 0 and printFormat.isNone:
+ echo(tr"downloading full package descriptions...")
+ let (aurPkgInfos, faerrors) = getAurPackageInfo(fullRpcInfos
+ .map(i => i.name), some(fullRpcInfos), proc (a: int, b: int) = discard)
+
+ if faerrors.len > 0:
+ for e in faerrors: printError(config.color, e)
+ 1
+ else:
+ let neededPacmanTargets = if printFormat.isNone and build and needed:
+ pacmanTargets.filter(target => (block:
+ let version = target.foundInfo.get.pkg.get.version
+ if checkNeeded(target.name, version):
+ true
+ else:
+ printWarning(config.color, tra("%s-%s is up to date -- skipping\n") %
+ [target.name, version])
+ false))
+ else:
+ pacmanTargets
+
+ let checkPacmanPkgInfos = printFormat.isNone and build and
+ neededPacmanTargets.len > 0
+
+ let (buildPkgInfos, obtainErrorMessages) = if checkPacmanPkgInfos: (block:
+ printColon(config.color, tr"Checking repositories...")
+ obtainBuildPkgInfos(config, pacmanTargets))
+ else:
+ (@[], @[])
+
+ if checkPacmanPkgInfos and buildPkgInfos.len < pacmanTargets.len:
+ for e in obtainErrorMessages: printError(config.color, e)
+ 1
+ else:
+ let pkgInfos = buildPkgInfos & aurPkgInfos
+ let targetsSet = fullTargets.map(t => t.name).toSet
+
+ let acceptedPkgInfos = pkgInfos.filter(pkgInfo => (block:
+ let instGroups = lc[x | (i <- installedTable.opt(pkgInfo.name),
+ x <- i.groups), string]
+
+ if config.ignored(pkgInfo.name, (instGroups & pkgInfo.groups).deduplicate):
+ if pkgInfo.name in targetsSet:
+ if printFormat.isNone:
+ let input = printColonUserChoice(config.color,
+ trp"%s is in IgnorePkg/IgnoreGroup. Install anyway?" % [pkgInfo.name],
+ ['y', 'n'], 'y', 'n', noconfirm, 'y')
+ input != 'n'
+ else:
+ true
+ else:
+ false
+ else:
+ true))
+
+ if acceptedPkgInfos.len > 0 and printFormat.isNone:
+ echo(trp("resolving dependencies...\n"))
+ let (satisfied, unsatisfied) = withAlpm(config.root, config.db,
+ config.dbs, config.arch, handle, dbs, errors):
+ findDependencies(config, handle, dbs, acceptedPkgInfos,
+ printFormat.isSome, noaur)
+
+ if unsatisfied.len > 0:
+ printUnsatisfied(config, satisfied, unsatisfied)
+ 1
+ else:
+ if printFormat.isNone:
+ let acceptedSet = acceptedPkgInfos.map(i => i.name).toSet
+
+ for pkgInfo in pkgInfos:
+ if not (pkgInfo.name in acceptedSet):
+ if not (pkgInfo.name in targetsSet) and upgradeCount > 0 and
+ installedTable.hasKey(pkgInfo.name):
+ printWarning(config.color, tra("%s: ignoring package upgrade (%s => %s)\n") %
+ [pkgInfo.name, installedTable[pkgInfo.name].version, pkgInfo.version])
+ else:
+ printWarning(config.color, trp("skipping target: %s\n") % [pkgInfo.name])
+ elif pkgInfo.repo == "aur" and pkgInfo.maintainer.isNone:
+ printWarning(config.color, tr"$# is orphaned" % [pkgInfo.name])
+
+ let aurPrintSet = acceptedPkgInfos.map(i => i.name).toSet
+ let fullPkgInfos = acceptedPkgInfos & lc[i | (s <- satisfied.values,
+ i <- s.buildPkgInfo, not (i.name in aurPrintSet)), PackageInfo].deduplicate
+
+ let directPacmanTargets = pacmanTargets.map(t => t.formatArgument)
+ let additionalPacmanTargets = lc[x.name | (x <- satisfied.values,
+ not x.installed and x.buildPkgInfo.isNone), string]
+ let orderedPkgInfos = orderInstallation(fullPkgInfos, satisfied)
+
+ let pacmanArgs = callArgs.filterExtensions(true, true)
+
+ if printFormat.isSome:
+ handlePrint(pacmanArgs, config, printFormat.unsafeGet, upgradeCount,
+ directPacmanTargets, additionalPacmanTargets, orderedPkgInfos)
+ else:
+ let explicits = if not args.check((none(string), "asdeps")):
+ targets.map(t => t.name)
+ else:
+ @[]
+
+ let passDirectPacmanTargets = if build: @[] else: directPacmanTargets
+
+ handleInstall(pacmanArgs, config, upgradeCount, noconfirm,
+ explicits.toSet, installed, satisfied, passDirectPacmanTargets,
+ additionalPacmanTargets, orderedPkgInfos)
diff --git a/src/feature/syncsearch.nim b/src/feature/syncsearch.nim
new file mode 100644
index 0000000..7b2e77c
--- /dev/null
+++ b/src/feature/syncsearch.nim
@@ -0,0 +1,53 @@
+import
+ algorithm, future, options, sequtils, strutils,
+ "../args", "../aur", "../config", "../common", "../format", "../package",
+ "../pacman", "../utils",
+ "../wrapper/alpm"
+
+proc handleSyncSearch*(args: seq[Argument], config: Config): int =
+ let (_, callArgs) = checkAndRefresh(config.color, args)
+
+ let quiet = args.check((some("q"), "quiet"))
+
+ let (aurPackages, aerrors) = findAurPackages(args.targets)
+ for e in aerrors: printError(config.color, e)
+
+ type Package = tuple[rpcInfo: RpcPackageInfo, installedVersion: Option[string]]
+
+ proc checkLocalPackages: seq[Package] =
+ if quiet:
+ aurPackages.map(pkg => (pkg, none(string)))
+ elif aurPackages.len > 0:
+ withAlpm(config.root, config.db, newSeq[string](), config.arch, handle, dbs, errors):
+ for e in errors: printError(config.color, e)
+
+ aurPackages.map(proc (rpcInfo: RpcPackageInfo): Package =
+ let pkg = handle.local[rpcInfo.name]
+ if pkg != nil:
+ (rpcInfo, some($pkg.version))
+ else:
+ (rpcInfo, none(string)))
+ else:
+ @[]
+
+ let pkgs = checkLocalPackages()
+ .sorted((a, b) => cmp(a.rpcInfo.name, b.rpcInfo.name))
+
+ var code = min(aerrors.len, 1)
+ if pkgs.len == 0:
+ if code == 0:
+ pacmanExec(false, config.color, callArgs)
+ else:
+ discard pacmanRun(false, config.color, callArgs)
+ code
+ else:
+ discard pacmanRun(false, config.color, callArgs)
+
+ for pkg in pkgs:
+ if quiet:
+ echo(pkg.rpcInfo.name)
+ else:
+ printPackageSearch(config.color, "aur", pkg.rpcInfo.name,
+ pkg.rpcInfo.version, pkg.installedVersion, pkg.rpcInfo.description,
+ some(formatPkgRating(pkg.rpcInfo.votes, pkg.rpcInfo.popularity)))
+ 0
diff --git a/src/format.nim b/src/format.nim
new file mode 100644
index 0000000..49dc5ba
--- /dev/null
+++ b/src/format.nim
@@ -0,0 +1,308 @@
+import
+ future, options, sequtils, strutils, times, unicode,
+ utils
+
+type
+ PackageLineFormat* = tuple[
+ title: string,
+ values: seq[string],
+ forceBreak: bool
+ ]
+
+ PackageInstallFormat* = tuple[
+ name: string,
+ repo: string,
+ oldVersion: Option[string],
+ newVersion: string
+ ]
+
+ CommentFormat* = tuple[
+ author: string,
+ date: string,
+ text: string
+ ]
+
+ Color {.pure.} = enum
+ normal = "\x1b[0m"
+ red = "\x1b[1;31m"
+ green = "\x1b[1;32m"
+ yellow = "\x1b[1;33m"
+ blue = "\x1b[1;34m"
+ magenta = "\x1b[1;35m"
+ cyan = "\x1b[1;36m"
+ bold = "\x1b[1;39m"
+
+template `^`(c: Color): string =
+ if color: $c else: ""
+
+type
+ WinSize = object
+ row: cushort
+ col: cushort
+ xpixel: cushort
+ ypixel: cushort
+
+proc ioctl[T](fd: cint, request: culong, argp: var T): cint
+ {.importc, header: "<sys/ioctl.h>".}
+
+proc getWindowSize(): tuple[width: int, height: int] =
+ var winSize: WinSize
+ if ioctl(1, 0x5413, winSize) != -1:
+ ((int) winSize.col, (int) winSize.row)
+ else:
+ (0, 0)
+
+proc formatPkgRating*(votes: int, popularity: float): string =
+ $votes & " / " & formatFloat(popularity, format = ffDecimal, precision = 6)
+
+proc computeMaxLength*(texts: openArray[string]): int =
+ texts.map(runeLen).max
+
+proc splitLines(text: string, lineSize: int, lines: seq[string] = @[]): seq[string] =
+ let addBreaks = lineSize >= 10
+
+ if not addBreaks:
+ lines & text
+ else:
+ let offset = text.runeOffset(lineSize)
+ if offset < 0:
+ lines & text
+ else:
+ let leftIndex = text.rfind(' ', offset - 1)
+ let rightIndex = text.find(' ', offset - 1)
+ let index = if leftIndex >= 0: leftIndex else: rightIndex
+
+ if index < 0:
+ lines & text
+ else:
+ text[index .. ^1].strip.splitLines(lineSize,
+ lines & text[0 .. index - 1].strip)
+
+proc printPackageInfo*(minPadding: int, color: bool, lines: varargs[PackageLineFormat]) =
+ let width = getWindowSize().width
+ let divider = " : "
+ let padding = max(lines.map(line => line.title.runeLen).max, minPadding)
+
+ let lineSize = width - (padding + divider.len)
+
+ proc formatTextLines(values: seq[string], forceBreak: bool): seq[string] =
+ if values.len == 0:
+ @[]
+ elif forceBreak:
+ lc[x | (y <- values.map(s => s.strip.splitLines(lineSize)), x <- y), string]
+ else:
+ values.map(v => v.strip).foldl(a & " " & b).splitLines(lineSize)
+
+ proc formatText(values: seq[string], forceBreak: bool): string =
+ let textSeq = formatTextLines(values, forceBreak)
+ if textSeq.len > 0:
+ textSeq.foldl(a & "\n" & ' '.repeat(padding + divider.len) & b)
+ else:
+ "None"
+
+ for line in lines:
+ let title = line.title & ' '.repeat(padding - line.title.runeLen) & divider
+ let text = formatText(line.values, line.forceBreak)
+ echo(^Color.bold, title, ^Color.normal, text)
+
+ # pacman leaves empty line in the end of info
+ echo()
+
+proc printPackageSearch*(color: bool, repo: string, name: string,
+ version: string, installedVersion: Option[string],
+ description: Option[string], extra: Option[string]) =
+ let commonText = ^Color.magenta & repo & "/" &
+ ^Color.bold & name & " " & ^Color.green & version & ^Color.normal
+
+ let installedText = if installedVersion == some(version):
+ " " & ^Color.cyan &
+ "[" & trp"installed" & "]" & ^Color.normal
+ elif installedVersion.isSome:
+ " " & ^Color.cyan &
+ "[" & trp"installed" & " " & installedVersion.unsafeGet &
+ "]" & ^Color.normal
+ else:
+ ""
+
+ let extraText = extra.map(e => " " & ^Color.yellow &
+ "[" & e & "]" & ^Color.normal).get("")
+
+ echo(commonText & installedText & extraText)
+
+ let padding = 4
+ let lines = description.get("").splitLines(getWindowSize().width - padding)
+ for line in lines:
+ echo(' '.repeat(padding), line)
+
+proc printPackages*(color: bool, verbose: bool, packages: seq[PackageInstallFormat]) =
+ if verbose:
+ let packageTitle = trp"Package" & " (" & $packages.len & ")"
+ let oldVersionTitle = trp"Old Version"
+ let newVersionTitle = trp"New Version"
+
+ let packageLen = max(packageTitle.runeLen,
+ packages.map(p => p.name.len + 1 + p.repo.len).max)
+
+ let oldVersionLenEmpty = packages.map(p => p.oldVersion.map(v => v.len).get(0)).max
+ let oldVersionLen = if oldVersionLenEmpty > 0:
+ max(oldVersionTitle.runeLen, oldVersionLenEmpty)
+ else:
+ 0
+
+ echo()
+ echo(^Color.bold & packageTitle &
+ ' '.repeat(packageLen - packageTitle.runeLen) &
+ (if oldVersionLen > 0: " " & oldVersionTitle &
+ ' '.repeat(oldVersionLen - oldVersionTitle.runeLen) else: "") &
+ " " & newVersionTitle & ^Color.normal)
+ echo()
+ for package in packages:
+ let name = package.repo & "/" & package.name
+ let oldVersion = package.oldVersion.get("")
+ echo(name & ' '.repeat(packageLen - name.runeLen) &
+ (if oldVersionLen > 0: " " & oldVersion &
+ ' '.repeat(oldVersionLen - oldVersion.len) else: "") &
+ " " & package.newVersion)
+ echo()
+ else:
+ let title = trp"Packages" & " (" & $packages.len & ") "
+ let padding = title.runeLen
+ let lines = packages.map(p => p.name & "-" & p.newVersion).foldl(a & " " & b)
+ .splitLines(getWindowSize().width - padding)
+
+ echo()
+ echo(^Color.bold, title, ^Color.normal, lines[0])
+ for line in lines[1 .. ^1]:
+ echo(' '.repeat(padding), line)
+ echo()
+
+proc printComments*(color: bool, maintainer: Option[string],
+ comments: seq[CommentFormat]) =
+ echo()
+ for comment in comments:
+ let badge = if maintainer == some(comment.author):
+ ^Color.cyan & "[maintainer]" & ^Color.normal & " "
+ else:
+ ""
+ echo(^Color.blue & comment.author & ^Color.normal & " " & badge &
+ ^Color.bold & comment.date & ^Color.normal)
+ echo(comment.text.replace("\n\n", "\n"))
+ echo()
+
+proc printError*(color: bool, s: string) =
+ stderr.writeLine(^Color.red, trp"error: ", ^Color.normal, s)
+
+proc printWarning*(color: bool, s: string) =
+ stderr.writeLine(^Color.yellow, trp"warning: ", ^Color.normal, s)
+
+proc printColon*(color: bool, s: string) =
+ echo(^Color.blue, ":: ", ^Color.bold, s, ^Color.normal)
+
+proc printColonUserInput*(color: bool, s: string,
+ noconfirm: bool, default: string, cancel: string): string =
+ stdout.write(^Color.blue, ":: ", ^Color.bold, s, ^Color.normal, " ")
+ stdout.flushFile()
+ if noconfirm:
+ echo()
+ default
+ else:
+ try:
+ stdin.readLine()
+ except EOFError:
+ cancel
+
+proc printColonUserChoice*(color: bool, s: string, answers: openArray[char],
+ positive: char, negative: char, noconfirm: bool, default: char): char =
+ let answersStr = answers
+ .map(c => (if c == positive: c.toUpperAscii else: c))
+ .foldl(a & "/" & $b, "")
+
+ let input = printColonUserInput(color, s & " [" & answersStr[1 .. ^1] & "]",
+ noconfirm, $default, $negative)
+ if input.len == 0:
+ positive
+ elif input.len == 1:
+ let c = input[0].toLowerAscii
+ if c in answers: c else: negative
+ else:
+ negative
+
+proc printUserInputHelp*(operations: varargs[tuple[answer: char, description: string]]) =
+ for operation in (@operations & ('?', tr"view this help")):
+ echo(" ", operation.answer, " - ", operation.description)
+
+proc printProgressFull*(bar: bool, title: string): ((string, float) -> void, () -> void) =
+ let width = getWindowSize().width
+
+ if not bar or width <= 0:
+ echo(title, "...")
+ (proc (a: string, c: float) {.closure.} = discard, proc {.closure.} = discard)
+ else:
+ let infoLen = max(width * 6 / 10, 50).int
+ let progressLen = width - infoLen
+ let startTime = getTime().toUnix
+
+ var lastTime = startTime
+ var lastProgress = 0f
+ var averageSpeed = -1f
+
+ proc update(prefix: string, progress: float) {.closure.} =
+ let progressTrim = max(min(1, progress + 0.005), 0)
+ let progressStr = $(progressTrim * 100).int & "%"
+ let paddedProgressStr = ' '.repeat(5 - progressStr.len) & progressStr
+
+ let indicator = if progressLen > 8: (block:
+ let fullLen = progressLen - 8
+ let barLen = (fullLen.float * progressTrim).int
+ " [" & '#'.repeat(barLen) & '-'.repeat(fullLen - barLen) & "]")
+ else:
+ ""
+
+ let time = getTime().toUnix
+ if progress > lastProgress and time > lastTime:
+ let speed = (progress - lastProgress) / (time - lastTime).float
+ lastTime = time
+ lastProgress = progress
+ if averageSpeed < 0:
+ averageSpeed = speed
+ else:
+ const factor = 0.25
+ averageSpeed = factor * speed + (1 - factor) * averageSpeed
+
+ let timeLeft = if averageSpeed > 0: (block:
+ let secondsLeft = ((1 - progress) / averageSpeed).int
+ let seconds = secondsLeft %% 60
+ let minutes = secondsLeft /% 60
+ let secondsStr = if seconds < 10: "0" & $seconds else: $seconds
+ let minutesStr = if minutes < 10: "0" & $minutes else: $minutes
+ minutesStr & ":" & secondsStr)
+ else:
+ "--:--"
+
+ stdout.write(prefix, title,
+ ' '.repeat(infoLen - prefix.runeLen - title.runeLen - 1 - timeLeft.len),
+ ' ', timeLeft, indicator, paddedProgressStr, "\x1b[0K\r")
+ stdout.flushFile()
+ discard
+
+ proc terminate() {.closure.} =
+ echo()
+ discard
+
+ update(" ", 0)
+
+ (update, terminate)
+
+proc printProgressShare*(bar: bool, title: string): ((int, int) -> void, () -> void) =
+ let (updateFull, terminate) = printProgressFull(bar, title)
+
+ proc update(current: int, total: int) {.closure.} =
+ let prefix = if total > 0:
+ "(" & ' '.repeat(($total).len - ($current).len) & $current & "/" &
+ $total & ") "
+ else:
+ " "
+
+ updateFull(prefix, current / total)
+
+ (update, terminate)
diff --git a/src/main.nim b/src/main.nim
new file mode 100644
index 0000000..9e98c30
--- /dev/null
+++ b/src/main.nim
@@ -0,0 +1,260 @@
+import
+ future, options, os, posix, re, sequtils, strutils,
+ args, config, format, pacman, utils
+
+import
+ "feature/syncinfo",
+ "feature/syncsearch",
+ "feature/syncinstall",
+ "feature/localquery"
+
+proc passValidation(args: seq[Argument], config: Config,
+ nonRootArgs: openArray[OptionPair], rootArgs: openArray[OptionPair],
+ opts: varargs[seq[CommandOption]]): int =
+ let checkArgs = args.filterOptions(true, false, true, opts)
+
+ if checkArgs.len == 0:
+ let needRoot = (nonRootArgs.len == 0 and args.check(rootArgs)) or
+ (nonRootArgs.len > 0 and (not args.check(nonRootArgs) or args.check(rootArgs)))
+ return pacmanExec(needRoot, config.color, args.filterExtensions(true, true))
+ else:
+ let extensions = args.filterExtensions(false, false)
+ if extensions.len == 0:
+ return pacmanExec(false, config.color, args)
+ else:
+ let arg = extensions[0]
+ if arg.isShort:
+ raise commandError(trp("invalid option '-%c'\n").strip
+ .replace("%c", "$#") % arg.key)
+ else:
+ raise commandError(trp("invalid option '--%s'\n").strip
+ .replace("%s", "$#") % arg.key)
+
+proc handleDatabase(args: seq[Argument], config: Config): int =
+ let nonRootArgs = [
+ (some("k"), "check")
+ ]
+
+ passValidation(args, config, nonRootArgs, [],
+ commonOptions, databaseOptions)
+
+proc handleFiles(args: seq[Argument], config: Config): int =
+ let rootArgs = [
+ (some("y"), "refresh")
+ ]
+
+ passValidation(args, config, [], rootArgs,
+ commonOptions, filesOptions)
+
+proc handleQuery(args: seq[Argument], config: Config): int =
+ let queryArgs = args.removeMatchOptions(commonOptions)
+
+ if queryArgs.checkOpGroup(OpGroup.localQuery) and
+ not queryArgs.check((some("e"), "explicit")) and
+ queryArgs.check((some("d"), "deps")) and
+ queryArgs.count((some("t"), "unrequired")) >= 3:
+ handleQueryOrphans(args, config)
+ else:
+ passValidation(args, config, [], [],
+ commonOptions, queryOptions)
+
+proc handleRemove(args: seq[Argument], config: Config): int =
+ let nonRootArgs = [
+ (some("p"), "print"),
+ (none(string), "print-format")
+ ]
+
+ passValidation(args, config, nonRootArgs, [],
+ commonOptions, transactionOptions, removeOptions)
+
+proc handleSync(args: seq[Argument], config: Config): int =
+ let syncArgs = args.removeMatchOptions(commonOptions, transactionOptions, upgradeOptions)
+ let conflict = args.checkConflicts(syncConflictingOptions)
+
+ if conflict.isSome:
+ let (left, right) = conflict.unsafeGet
+ printError(config.color, trp("invalid option: '%s' and '%s' may not be used together\n") %
+ ["--" & left, "--" & right])
+ 1
+ elif syncArgs.check((some("i"), "info")) and
+ syncArgs.checkOpGroup(OpGroup.syncQuery):
+ handleSyncInfo(args, config)
+ elif syncArgs.check((some("s"), "search")) and
+ syncArgs.checkOpGroup(OpGroup.syncSearch):
+ handleSyncSearch(args, config)
+ elif syncArgs.checkOpGroup(OpGroup.syncInstall) and
+ (args.check((some("u"), "sysupgrade")) or args.targets.len > 0):
+ let isNonDefaultRoot = not config.isRootDefault
+ let isDowngrade = args.count((some("u"), "sysupgrade")) >= 2
+ let isSkipDeps = args.check((some("d"), "nodeps"))
+ let isRoot = getuid() == 0
+
+ let build = args.check((none(string), "build"))
+ let noaur = args.check((none(string), "noaur"))
+
+ let noBuild = isNonDefaultRoot or isDowngrade or isSkipDeps or isRoot
+
+ if build and noBuild:
+ if isNonDefaultRoot:
+ printError(config.color, tr"non-default root path is specified" & " -- " &
+ tr"building is not allowed")
+ elif isDowngrade:
+ printError(config.color, tr"downgrades are enabled" & " -- " &
+ tr"building is not allowed")
+ elif isSkipDeps:
+ printError(config.color, tr"dependency check is skipped" & " -- " &
+ tr"building is not allowed")
+ elif isRoot:
+ printError(config.color, tr"running as root" & " -- " &
+ tr"building is not allowed")
+ 1
+ else:
+ let noaurAdd = noBuild and not noaur
+
+ if noaurAdd:
+ if isNonDefaultRoot:
+ printWarning(config.color, tr"non-default root path is specified" & " -- " &
+ tr"'$#' is assumed" % ["--noaur"])
+ elif isDowngrade:
+ printWarning(config.color, tr"downgrades are enabled" & " -- " &
+ tr"'$#' is assumed" % ["--noaur"])
+ elif isSkipDeps:
+ printWarning(config.color, tr"dependency check is skipped" & " -- " &
+ tr"'$#' is assumed" % ["--noaur"])
+ elif isRoot:
+ printWarning(config.color, tr"running as root" & " -- " &
+ tr"'$#' is assumed" % ["--noaur"])
+
+ if noaurAdd:
+ handleSyncInstall(args & ("noaur", none(string), ArgumentType.long), config)
+ else:
+ handleSyncInstall(args, config)
+ else:
+ let nonRootArgs = [
+ (some("p"), "print"),
+ (none(string), "print-format"),
+ (some("g"), "groups"),
+ (some("i"), "info"),
+ (some("l"), "list"),
+ (some("s"), "search")
+ ]
+
+ let rootArgs = [
+ (some("y"), "refresh"),
+ ]
+
+ passValidation(args, config, nonRootArgs, rootArgs,
+ commonOptions, transactionOptions, upgradeOptions, syncOptions)
+
+proc handleDeptest(args: seq[Argument], config: Config): int =
+ passValidation(args, config, [], [], commonOptions)
+
+proc handleUpgrade(args: seq[Argument], config: Config): int =
+ let nonRootArgs = [
+ (some("p"), "print"),
+ (none(string), "print-format")
+ ]
+
+ passValidation(args, config, nonRootArgs, [],
+ commonOptions, transactionOptions, upgradeOptions)
+
+proc handleHelp(operation: OperationType) =
+ proc printHelp(command: string, text: string) =
+ echo(' '.repeat(6), "--", command, ' '.repeat(15 - command.len), text)
+
+ let operationArgs = operations
+ .filter(o => o.otype == operation)
+ .map(o => @["-" & o.pair.short.get])
+ .optFirst.get(@[]) & @["-h"]
+
+ let lines = runProgram(pacmanCmd & operationArgs)
+
+ for line in lines:
+ echo(line.replace(re"\bpacman\b", "pakku"))
+
+ if lines.len > 0:
+ case operation:
+ of OperationType.sync:
+ printHelp("build", tr"build targets from source")
+ printHelp("noaur", tr"disable all AUR operations")
+ else:
+ discard
+
+const
+ version = $getenv("PROG_VERSION")
+ copyright = $getenv("PROG_COPYRIGHT")
+
+proc handleVersion(): int =
+ echo()
+ echo(' '.repeat(23), "Pakku v", version)
+ echo(' '.repeat(23), "Copyright (C) ", copyright)
+ pacmanExec(false, false, ("V", none(string), ArgumentType.short))
+
+proc signal(sign: cint, handler: pointer): pointer
+ {.importc, header: "<signal.h>".}
+
+discard setlocale(LC_ALL, "")
+discard signal(SIGINT, cast[pointer](SIG_DFL))
+
+template withErrorHandler(propColor: Option[bool], T: typedesc, body: untyped):
+ tuple[success: Option[T], code: int] =
+ try:
+ (some(body), 0)
+ except HaltError:
+ let e = (ref HaltError) getCurrentException()
+ (none(T), e.code)
+ except CommandError:
+ let e = (ref CommandError) getCurrentException()
+ if e.error:
+ printError(e.color.orElse(propColor).get(false), e.msg)
+ else:
+ stderr.writeLine(e.msg)
+ (none(T), 1)
+
+let init = withErrorHandler(none(bool),
+ tuple[parsedArgs: seq[Argument], config: Config]):
+ let parsedArgs = splitArgs(commandLineParams(), optionsWithParameter)
+ let pacmanConfig = obtainPacmanConfig(parsedArgs)
+ let config = obtainConfig(pacmanConfig)
+ (parsedArgs, config)
+
+proc run(parsedArgs: seq[Argument], config: Config):
+ tuple[success: Option[int], code: int] =
+ withErrorHandler(some(config.color), int):
+ let operation = getOperation(parsedArgs)
+ if operation != OperationType.invalid and
+ parsedArgs.check((some("h"), "help")):
+ handleHelp(operation)
+ 0
+ elif operation != OperationType.invalid and
+ parsedArgs.check((some("V"), "version")):
+ handleVersion()
+ else:
+ case operation:
+ of OperationType.database:
+ handleDatabase(parsedArgs, config)
+ of OperationType.files:
+ handleFiles(parsedArgs, config)
+ of OperationType.query:
+ handleQuery(parsedArgs, config)
+ of OperationType.remove:
+ handleRemove(parsedArgs, config)
+ of OperationType.sync:
+ handleSync(parsedArgs, config)
+ of OperationType.deptest:
+ handleDeptest(parsedArgs, config)
+ of OperationType.upgrade:
+ handleUpgrade(parsedArgs, config)
+ else:
+ pacmanExec(false, config.color,
+ parsedArgs.filterExtensions(true, true))
+
+let runResult = if init.success.isSome:
+ run(init.success.unsafeGet.parsedArgs, init.success.unsafeGet.config)
+ else:
+ (none(int), init.code)
+
+programResult = if runResult.success.isSome:
+ runResult.success.unsafeGet
+ else:
+ runResult.code
diff --git a/src/package.nim b/src/package.nim
new file mode 100644
index 0000000..699aee5
--- /dev/null
+++ b/src/package.nim
@@ -0,0 +1,249 @@
+import
+ future, options, os, re, sequtils, sets, strutils, tables,
+ utils
+
+type
+ ConstraintOperation* {.pure.} = enum
+ ge = ">=",
+ gt = ">",
+ eq = "=",
+ lt = "<",
+ le = "<="
+
+ VersionConstraint* = tuple[
+ operation: ConstraintOperation,
+ version: string
+ ]
+
+ PackageReference* = tuple[
+ name: string,
+ description: Option[string],
+ constraint: Option[VersionConstraint]
+ ]
+
+ ArchPackageReference* = tuple[
+ arch: Option[string],
+ reference: PackageReference
+ ]
+
+ RpcPackageInfo* = object of RootObj
+ repo*: string
+ base*: string
+ name*: string
+ version*: string
+ description*: Option[string]
+ maintainer*: Option[string]
+ firstSubmitted*: Option[int64]
+ lastModified*: Option[int64]
+ votes*: int
+ popularity*: float
+
+ PackageInfo* = object of RpcPackageInfo
+ archs*: seq[string]
+ url*: Option[string]
+ licenses*: seq[string]
+ groups*: seq[string]
+ depends*: seq[ArchPackageReference]
+ makeDepends*: seq[ArchPackageReference]
+ checkDepends*: seq[ArchPackageReference]
+ optional*: seq[ArchPackageReference]
+ provides*: seq[ArchPackageReference]
+ conflicts*: seq[ArchPackageReference]
+ replaces*: seq[ArchPackageReference]
+ gitUrl*: string
+ gitBranch*: Option[string]
+ gitCommit*: Option[string]
+ gitPath*: Option[string]
+
+ GitRepo* = tuple[
+ url: string,
+ branch: string,
+ path: string
+ ]
+
+ PackageRepo = tuple[
+ os: HashSet[string],
+ repo: HashSet[string],
+ git: GitRepo
+ ]
+
+ SrcInfoPair = tuple[key: string, value: string]
+
+const
+ packageRepos: seq[PackageRepo] = @[
+ (["arch"].toSet,
+ ["core", "extra", "testing"].toSet,
+ ("https://git.archlinux.org/svntogit/packages.git",
+ "packages/${BASE}", "repos/${REPO}-${ARCH}")),
+ (["arch"].toSet,
+ ["community", "community-testing", "multilib", "multilib-testing"].toSet,
+ ("https://git.archlinux.org/svntogit/community.git",
+ "packages/${BASE}", "repos/${REPO}-${ARCH}"))
+ ]
+
+static:
+ # test only single match available
+ let osSet = lc[x | (r <- packageRepos, x <- r.os), string].toSet
+ let repoSet = lc[x | (r <- packageRepos, x <- r.repo), string].toSet
+ for os in osSet:
+ for repo in repoSet:
+ let osValue = os
+ let repoValue = repo
+ if packageRepos.filter(pr => osValue in pr.os and repoValue in pr.repo).len >= 2:
+ raise newException(SystemError,
+ "only single matching repo available: " & os & ":" & repo)
+
+proc readOsId: string =
+ var file: File
+ if file.open("/usr/bin/os-release"):
+ try:
+ while true:
+ let rawLine = readLine(file)
+ if rawLine[0 .. 2] == "ID=":
+ return rawLine[3 .. ^1]
+ except EOFError:
+ discard
+ except IOError:
+ discard
+ finally:
+ file.close()
+ return "arch"
+
+let osId = readOsId()
+
+proc lookupGitRepo*(repo: string, base: string, arch: string): Option[GitRepo] =
+ template replaceAll(gitPart: string): string =
+ gitPart
+ .replace("${REPO}", repo)
+ .replace("${BASE}", base)
+ .replace("${ARCH}", arch)
+
+ packageRepos
+ .filter(pr => osId in pr.os and repo in pr.repo)
+ .map(pr => (pr.git.url.replaceAll, pr.git.branch.replaceAll, pr.git.path.replaceAll))
+ .optFirst
+
+template repoPath*(tmpRoot: string, base: string): string =
+ tmpRoot & "/" & base
+
+template buildPath*(repoPath: string, gitPath: Option[string]): string =
+ gitPath.map(p => repoPath & "/" & p).get(repoPath)
+
+template allDepends*(pkgInfo: PackageInfo): seq[ArchPackageReference] =
+ pkgInfo.depends & pkgInfo.makeDepends & pkgInfo.checkDepends
+
+proc parseSrcInfoKeys(srcInfo: string):
+ tuple[baseSeq: ref seq[SrcInfoPair], table: Table[string, ref seq[SrcInfoPair]]] =
+ var table = initTable[string, ref seq[SrcInfoPair]]()
+ var matches: array[2, string]
+ var baseSeq: ref seq[SrcInfoPair]
+ var values: ref seq[SrcInfoPair]
+
+ new(baseSeq)
+ baseSeq[] = newSeq[SrcInfoPair]()
+
+ for line in srcInfo.splitLines:
+ if line.match(re"[\t\ ]*(\w+)\ =\ (.*)", matches):
+ let key = matches[0]
+ let value = matches[1]
+
+ if key == "pkgbase":
+ values = baseSeq
+ elif key == "pkgname":
+ if table.hasKey(value):
+ values = table[value]
+ else:
+ new(values)
+ values[] = newSeq[SrcInfoPair]()
+ table[value] = values
+
+ if values != nil:
+ values[] &= (key: key, value: value)
+
+ (baseSeq: baseSeq, table: table)
+
+proc parseSrcInfoName(repo: string, name: string, rpcInfos: seq[RpcPackageInfo],
+ baseSeq: ref seq[SrcInfoPair], nameSeq: ref seq[SrcInfoPair],
+ gitUrl: string, gitBranch: Option[string], gitCommit: Option[string],
+ gitPath: Option[string]): Option[PackageInfo] =
+ let pairs = baseSeq[] & nameSeq[]
+ proc collect(keyName: string): seq[string] =
+ lc[x.value | (x <- pairs, x.key == keyName), string]
+
+ proc splitConstraint(name: string): PackageReference =
+ var matches: array[3, string]
+
+ let descIndex = name.find(": ")
+ let (description, workName) = if descIndex >= 0:
+ (some(name[descIndex + 2 .. ^1]), name[0 .. descIndex - 1])
+ else:
+ (none(string), name)
+
+ if workName.match(re"([^><=]*)\ *(>|<|=|>=|<=)\ *([^ ]*)", matches):
+ let constraints = toSeq(enumerate[ConstraintOperation]())
+ let index = constraints.map(s => $s).find(matches[1])
+
+ if index >= 0:
+ (matches[0], description, some((constraints[index], matches[2])))
+ else:
+ (matches[0], description, none(VersionConstraint))
+ else:
+ (workName, description, none(VersionConstraint))
+
+ proc collectArch(keyName: string, arch: Option[string]): seq[ArchPackageReference] =
+ collect(arch.map(a => keyName & "_" & a).get(keyName))
+ .map(splitConstraint)
+ .map(c => (arch, (c.name, c.description, c.constraint)))
+
+ proc collectArchs(keyName: string, archs: seq[string]): seq[ArchPackageReference] =
+ let archsFull = none(string) & archs.map(some)
+ lc[x | (a <- archsFull, x <- collectArch(keyName, a)), ArchPackageReference]
+
+ let base = lc[x.value | (x <- baseSeq[], x.key == "pkgbase"), string].optLast
+
+ let version = collect("pkgver").optLast
+ let release = collect("pkgrel").optLast
+ let epoch = collect("epoch").optLast
+ let versionFull = lc[(v & "-" & r) | (v <- version, r <- release), string].optLast
+ .map(v => epoch.map(e => e & ":" & v).get(v))
+
+ let description = collect("pkgdesc").optLast
+ let archs = collect("arch").filter(a => a != "any")
+ let url = collect("url").optLast
+ let licenses = collect("license")
+ let groups = collect("groups")
+
+ let depends = collectArchs("depends", archs)
+ let makeDepends = collectArchs("makedepends", archs)
+ let checkDepends = collectArchs("checkdepends", archs)
+ let optional = collectArchs("optdepends", archs)
+ let provides = collectArchs("provides", archs)
+ let conflicts = collectArchs("conflicts", archs)
+ let replaces = collectArchs("replaces", archs)
+
+ let info = rpcInfos.filter(i => i.name == name).optLast
+
+ lc[PackageInfo(repo: repo, base: b, name: name, version: v, description: description,
+ archs: archs, url: url, licenses: licenses, groups: groups,
+ depends: depends, makeDepends: makeDepends, checkdepends: checkDepends,
+ optional: optional, provides: provides, conflicts: conflicts, replaces: replaces,
+ maintainer: info.map(i => i.maintainer).flatten,
+ firstSubmitted: info.map( i => i.firstSubmitted).flatten,
+ lastModified: info.map( i => i.lastModified).flatten,
+ votes: info.map(i => i.votes).get(0),
+ popularity: info.map(i => i.popularity).get(0),
+ gitUrl: gitUrl, gitBranch: gitBranch, gitCommit: gitCommit, gitPath: gitPath) |
+ (b <- base, v <- versionFull), PackageInfo].optLast
+
+proc parseSrcInfo*(repo: string, srcInfo: string,
+ gitUrl: string, gitBranch: Option[string], gitCommit: Option[string],
+ gitPath: Option[string], rpcInfos: seq[RpcPackageInfo] = @[]): seq[PackageInfo] =
+ let parsed = parseSrcInfoKeys(srcInfo)
+ let packageSeq = toSeq(parsed.table.namedPairs)
+ lc[x | (pair <- packageSeq, x <- parseSrcInfoName(repo, pair.key, rpcInfos,
+ parsed.baseSeq, pair.value, gitUrl, gitBranch, gitCommit, gitPath)), PackageInfo]
+
+proc `$`*(reference: PackageReference): string =
+ reference.constraint
+ .map(c => reference.name & $c.operation & c.version)
+ .get(reference.name)
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
diff --git a/src/utils.nim b/src/utils.nim
new file mode 100644
index 0000000..1d7c6bd
--- /dev/null
+++ b/src/utils.nim
@@ -0,0 +1,203 @@
+import
+ future, hashes, options, os, osproc, posix, strutils, tables
+
+type
+ HaltError* = object of Exception
+ code*: int
+
+ CommandError* = object of Exception
+ color*: Option[bool]
+ error*: bool
+
+const
+ pkgLibDir* = getenv("PROG_PKGLIBDIR")
+ localStateDir* = getenv("PROG_LOCALSTATEDIR")
+ sysConfDir* = getenv("PROG_SYSCONFDIR")
+
+ bashCmd* = "/bin/bash"
+ suCmd* = "/usr/bin/su"
+ sudoCmd* = "/usr/bin/sudo"
+ gitCmd* = "/usr/bin/git"
+ pacmanCmd* = "/usr/bin/pacman"
+ makepkgCmd* = "/usr/bin/makepkg"
+
+template haltError*(code: int): untyped =
+ var e: ref HaltError
+ new(e)
+ e.code = code
+ e
+
+template commandError*(message: string, colorNeeded: Option[bool] = none(bool),
+ showError: bool = true): untyped =
+ var e: ref CommandError
+ new(e)
+ e.msg = message
+ e.color = colorNeeded
+ e.error = showError
+ e
+
+iterator items*[T](self: Option[T]): T {.raises: [].} =
+ if self.isSome:
+ yield self.unsafeGet
+
+template len*[T](self: Option[T]): int =
+ if self.isSome: 1 else: 0
+
+template orElse*[T, R](opt1: Option[T], opt2: Option[R]): Option[R] =
+ if opt1.isSome: opt1 else: opt2
+
+template hash*[T](opt: Option[T]): int =
+ opt.map(hash).get(0)
+
+proc opt*[K, V](table: Table[K, V], key: K): Option[V] =
+ if table.hasKey(key): some(table[key]) else: none(V)
+
+proc opt*[K, V](table: OrderedTable[K, V], key: K): Option[V] =
+ if table.hasKey(key): some(table[key]) else: none(V)
+
+proc optFirst*[T](s: openArray[T]): Option[T] =
+ if s.len > 0: some(s[s.low]) else: none(T)
+
+proc optLast*[T](s: openArray[T]): Option[T] =
+ if s.len > 0: some(s[s.high]) else: none(T)
+
+iterator enumerate*[T: enum]: T =
+ let elow = T.low.ord
+ let ehigh = T.high.ord
+ for i in elow .. ehigh:
+ yield T(i)
+
+iterator namedPairs*[K, V](table: Table[K, V]): tuple[key: K, value: V] =
+ for key, value in table.pairs:
+ yield (key, value)
+
+iterator reversed*[T](s: openArray[T]): T =
+ for i in countdown(s.len - 1, 0):
+ yield s[i]
+
+proc groupBy*[T, X](s: seq[T], callback: T -> X): seq[tuple[key: X, values: seq[T]]] =
+ var table = initOrderedTable[X, ref seq[T]]()
+ for value in s:
+ let key = callback(value)
+ var work: ref seq[T]
+ if table.hasKey(key):
+ work = table[key]
+ else:
+ new(work)
+ work[] = newSeq[T]()
+ table[key] = work
+ work[] &= value
+
+ result = newSeq[tuple[key: X, values: seq[T]]]()
+ for key, values in table.pairs:
+ result &= (key, values[])
+
+proc perror*(s: cstring): void {.importc, header: "<stdio.h>".}
+template perror*: void = perror(getAppFilename())
+
+proc execResult*(args: varargs[string]): int =
+ let cexec = allocCStringArray(args)
+ let code = execvp(cexec[0], cexec)
+ perror()
+ deallocCStringArray(cexec)
+ code
+
+const
+ interruptSignals* = [SIGINT, SIGTERM]
+
+template blockSignals*(signals: openArray[cint],
+ unblock: untyped, body: untyped): untyped =
+ block:
+ var sigset: Sigset
+ var sigoldset: Sigset
+
+ discard sigemptyset(sigset)
+ for s in signals:
+ discard sigaddset(sigset, s)
+ discard sigprocmask(SIG_BLOCK, sigset, sigoldset)
+
+ var unblocked = false
+ let unblock = () => (block:
+ if not unblocked:
+ discard sigprocmask(SIG_SETMASK, sigoldset, sigset)
+ unblocked = true)
+
+ try:
+ body
+ finally:
+ unblock()
+
+proc forkWait*(call: () -> int): int =
+ blockSignals(interruptSignals, unblock):
+ let pid = fork()
+ if pid == 0:
+ unblock()
+ quit(call())
+ else:
+ var status: cint = 1
+ discard waitpid(pid, status, 0)
+ if WIFEXITED(status):
+ return WEXITSTATUS(status)
+ else:
+ discard kill(getpid(), status)
+ return 1
+
+proc runProgram*(args: varargs[string]): seq[string] =
+ let output = execProcess(args[0], @args[1 .. ^1], options = {})
+ if output.len == 0:
+ @[]
+ elif output.len > 0 and $output[^1] == "\n":
+ output[0 .. ^2].split("\n")
+ else:
+ output.split("\n")
+
+proc setenv*(name: cstring, value: cstring, override: cint): cint
+ {.importc, header: "<stdlib.h>".}
+
+proc getUser*: (int, string) =
+ let uid = getuid()
+ while true:
+ var pw = getpwent()
+ if pw == nil:
+ raise newException(SystemError, "")
+ if pw.pw_uid == uid:
+ return (uid.int, $pw.pw_name)
+
+proc toString*[T](arr: array[T, char], length: Option[int]): string =
+ var workLength = length.get(T.high + 1)
+ var str = newStringOfCap(workLength)
+ for i in 0 .. workLength - 1:
+ let c = arr[i]
+ if length.isNone and c == '\0':
+ break
+ str.add(arr[i])
+ str
+
+proc removeDirQuiet*(s: string) =
+ try:
+ removeDir(s)
+ except:
+ discard
+
+proc dgettext(domain: cstring, s: cstring): cstring
+ {.cdecl, importc: "dgettext".}
+
+proc gettext(domain: string, s: string): string =
+ let translated = dgettext(domain, s)
+ if translated != nil: $translated else: s
+
+proc gettextHandle(domain: string, s: string): string =
+ let res = gettext(domain, s).replace("%s", "$#").replace("%c", "$#")
+ if res.len > 0 and res[^1 .. ^1] == "\n": res[0 .. ^2] else: res
+
+template tr*(s: string): string =
+ gettext("pakku", s)
+
+template trp*(s: string): string =
+ gettextHandle("pacman", s)
+
+template tra*(s: string): string =
+ gettextHandle("libalpm", s)
+
+template trc*(s: string): string =
+ gettextHandle("libc", s)
diff --git a/src/wrapper/alpm.nim b/src/wrapper/alpm.nim
new file mode 100644
index 0000000..00b5b86
--- /dev/null
+++ b/src/wrapper/alpm.nim
@@ -0,0 +1,146 @@
+import
+ strutils,
+ "../utils"
+
+type
+ AlpmHandle* = object
+ AlpmDatabase* = object
+ AlpmPackage* = object
+
+ AlpmList*[T] = object
+ data*: T
+ prev*: ptr AlpmList[T]
+ next*: ptr AlpmList[T]
+
+ AlpmDepMod* {.pure, size: sizeof(cint).} = enum
+ no = 1,
+ eq = 2,
+ ge = 3,
+ le = 4,
+ gt = 5,
+ lt = 6
+
+ AlpmReason* {.pure, size: sizeof(cint).} = enum
+ explicit = 0,
+ depend = 1
+
+ AlpmDependency* = object
+ name*: cstring
+ version*: cstring
+ desc*: cstring
+ nameHash: culong
+ depMod*: AlpmDepMod
+
+ AlpmGroup* = object
+ name*: cstring
+ packages*: ptr AlpmList[ptr AlpmPackage]
+
+{.passL: "-lalpm".}
+
+proc newAlpmHandle*(root: cstring, dbpath: cstring, err: var cint): ptr AlpmHandle
+ {.cdecl, importc: "alpm_initialize".}
+
+proc release*(handle: ptr AlpmHandle): cint
+ {.cdecl, importc: "alpm_release".}
+
+proc setArch*(handle: ptr AlpmHandle, arch: cstring): cint
+ {.cdecl, importc: "alpm_option_set_arch".}
+
+proc vercmp*(a: cstring, b: cstring): cint
+ {.cdecl, importc: "alpm_pkg_vercmp".}
+
+proc errno*(handle: ptr AlpmHandle): cint
+ {.cdecl, importc: "alpm_errno".}
+
+proc errorAlpm*(errno: cint): cstring
+ {.cdecl, importc: "alpm_strerror".}
+
+proc register*(handle: ptr AlpmHandle, treeName: cstring, level: cint): ptr AlpmDatabase
+ {.cdecl, importc: "alpm_register_syncdb".}
+
+proc local*(handle: ptr AlpmHandle): ptr AlpmDatabase
+ {.cdecl, importc: "alpm_get_localdb".}
+
+proc packages*(db: ptr AlpmDatabase): ptr AlpmList[ptr AlpmPackage]
+ {.cdecl, importc: "alpm_db_get_pkgcache".}
+
+proc groups*(db: ptr AlpmDatabase): ptr AlpmList[ptr AlpmGroup]
+ {.cdecl, importc: "alpm_db_get_groupcache".}
+
+proc name*(db: ptr AlpmDatabase): cstring
+ {.cdecl, importc: "alpm_db_get_name".}
+
+proc `[]`*(db: ptr AlpmDatabase, name: cstring): ptr AlpmPackage
+ {.cdecl, importc: "alpm_db_get_pkg".}
+
+proc base*(pkg: ptr AlpmPackage): cstring
+ {.cdecl, importc: "alpm_pkg_get_base".}
+
+proc name*(pkg: ptr AlpmPackage): cstring
+ {.cdecl, importc: "alpm_pkg_get_name".}
+
+proc version*(pkg: ptr AlpmPackage): cstring
+ {.cdecl, importc: "alpm_pkg_get_version".}
+
+proc arch*(pkg: ptr AlpmPackage): cstring
+ {.cdecl, importc: "alpm_pkg_get_arch".}
+
+proc groups*(pkg: ptr AlpmPackage): ptr AlpmList[cstring]
+ {.cdecl, importc: "alpm_pkg_get_groups".}
+
+proc reason*(pkg: ptr AlpmPackage): AlpmReason
+ {.cdecl, importc: "alpm_pkg_get_reason".}
+
+proc depends*(pkg: ptr AlpmPackage): ptr AlpmList[ptr AlpmDependency]
+ {.cdecl, importc: "alpm_pkg_get_depends".}
+
+proc optional*(pkg: ptr AlpmPackage): ptr AlpmList[ptr AlpmDependency]
+ {.cdecl, importc: "alpm_pkg_get_optdepends".}
+
+proc provides*(pkg: ptr AlpmPackage): ptr AlpmList[ptr AlpmDependency]
+ {.cdecl, importc: "alpm_pkg_get_provides".}
+
+proc cfree*(data: pointer)
+ {.cdecl, importc: "free", header: "<stdlib.h>".}
+
+proc freeList*[T](list: ptr AlpmList[T])
+ {.cdecl, importc: "alpm_list_free".}
+
+proc freeListInner*[T](list: ptr AlpmList[T], fn: proc (data: pointer): void {.cdecl.})
+ {.cdecl, importc: "alpm_list_free_inner".}
+
+proc freeListFull*[T](list: ptr AlpmList[T]) =
+ list.freeListInner(cfree)
+ list.freeList()
+
+template withAlpm*(root: string, db: string, dbs: seq[string], arch: string,
+ handle: untyped, alpmDbs: untyped, errors: untyped, body: untyped): untyped =
+ block:
+ var errno: cint = 0
+ let handle = newAlpmHandle(root, db, errno)
+
+ if handle == nil:
+ raise commandError(trp("failed to initialize alpm library\n(%s: %s)\n").strip
+ .replace("%s", "$#") % [$errno.errorAlpm, db])
+
+ var alpmDbs = newSeq[ptr AlpmDatabase]()
+ var errors = newSeq[string]()
+ for dbName in dbs:
+ let alpmDb = handle.register(dbName, 1 shl 30)
+ if alpmDb != nil:
+ alpmDbs &= alpmDb
+ else:
+ errors &= trp("could not register '%s' database (%s)\n").strip
+ .replace("%s", "$#") % [dbName, $handle.errno.errorAlpm]
+
+ try:
+ discard handle.setArch(arch)
+ body
+ finally:
+ discard handle.release()
+
+iterator items*[T](list: ptr AlpmList[T]): T =
+ var listi = list
+ while listi != nil:
+ yield listi.data
+ listi = listi.next
diff --git a/src/wrapper/curl.nim b/src/wrapper/curl.nim
new file mode 100644
index 0000000..5f3e814
--- /dev/null
+++ b/src/wrapper/curl.nim
@@ -0,0 +1,123 @@
+import
+ strutils,
+ "../utils"
+
+type
+ CurlHandle* = object
+
+ CurlInstance* = object
+ handle: ptr CurlHandle
+ data: seq[char]
+
+ CurlOption {.pure, size: sizeof(cint).} = enum
+ followLocation = 52,
+ noSignal = 99,
+ timeoutMs = 155,
+ connectTimeoutMs = 156,
+ writeData = 10001,
+ url = 10002,
+ writeFunction = 20011
+
+ CurlError* = object of Exception
+
+{.passL: "-lcurl".}
+
+proc initCurlGlobal*(flags: clong): cint
+ {.cdecl, importc: "curl_global_init".}
+
+proc cleanupCurlGlobal*: void
+ {.cdecl, importc: "curl_global_cleanup".}
+
+proc newCurlHandle*: ptr CurlHandle
+ {.cdecl, importc: "curl_easy_init".}
+
+proc cleanup*(handle: ptr CurlHandle)
+ {.cdecl, importc: "curl_easy_cleanup".}
+
+proc errorCurl*(error: cint): cstring
+ {.cdecl, importc: "curl_easy_strerror".}
+
+proc setOption*(handle: ptr CurlHandle, option: CurlOption): cint
+ {.cdecl, importc: "curl_easy_setopt", varargs.}
+
+proc perform*(handle: ptr CurlHandle): cint
+ {.cdecl, importc: "curl_easy_perform".}
+
+proc escape*(handle: ptr CurlHandle, input: cstring, length: cint): cstring
+ {.cdecl, importc: "curl_easy_escape".}
+
+proc freeCurl*(data: pointer)
+ {.cdecl, importc: "curl_free".}
+
+proc escape*(instance: ref CurlInstance, s: string): string =
+ let esc = instance.handle.escape(s, 0)
+ if esc != nil:
+ let nesc = $esc
+ freeCurl(esc)
+ nesc
+ else:
+ ""
+
+proc curlWriteMemory(mem: array[csize.high, char], size: csize, nmemb: csize,
+ userdata: ref CurlInstance): csize {.cdecl.} =
+ let total = size * nmemb
+ if total > 0:
+ userData.data &= mem[0 .. total - 1]
+ total
+
+var refCount = 0
+
+template withCurlGlobal*(body: untyped): untyped =
+ block:
+ if refCount == 0:
+ if initCurlGlobal(0) != 0:
+ raise commandError(tr"failed to initialize curl library")
+ refCount += 1
+ try:
+ body
+ finally:
+ refCount -= 1
+ if refCount == 0:
+ cleanupCurlGlobal()
+
+template withCurl*(instance: untyped, body: untyped): untyped =
+ block:
+ let handle = newCurlHandle()
+ if handle == nil:
+ raise commandError(tr"failed to initialize curl handle")
+
+ var instance: ref CurlInstance
+ new(instance)
+ instance.handle = handle
+ instance.data = newSeq[char]()
+
+ proc raiseError(code: cint) =
+ if code != 0:
+ let msg = code.errorCurl
+ if msg != nil:
+ raise newException(CurlError, tr"failed to perform request" & (": $#" % [$msg]))
+ else:
+ raise newException(CurlError, tr"failed to perform request")
+
+ proc performInternal(url: string): seq[char] =
+ raiseError(handle.setOption(CurlOption.followLocation, (clong) 1))
+ raiseError(handle.setOption(CurlOption.noSignal, (clong) 1))
+ raiseError(handle.setOption(CurlOption.timeoutMs, (clong) 15000))
+ raiseError(handle.setOption(CurlOption.connectTimeoutMs, (clong) 15000))
+ raiseError(handle.setOption(CurlOption.url, url))
+ raiseError(handle.setOption(CurlOption.writeFunction, cast[pointer](curlWriteMemory)))
+ raiseError(handle.setOption(CurlOption.writeData, instance))
+ raiseError(handle.perform())
+ instance.data
+
+ proc performString(url: string): string =
+ let data = performInternal(url)
+ var str = newStringOfCap(data.len)
+ for c in data:
+ str.add(c)
+ str
+
+ try:
+ body
+ finally:
+ handle.cleanup()