diff --git a/doc/pakku.conf.5.txt b/doc/pakku.conf.5.txt index df1c0c2..6c09750 100644 --- a/doc/pakku.conf.5.txt +++ b/doc/pakku.conf.5.txt @@ -24,6 +24,13 @@ Options Set the temporary directory in which pakku will perform all building operations. The default value is +/tmp/pakku-$\{USER\}+. +*PackageOutputDir =* path/to/package/output/dir:: + Built package archives (\fB*.pkg.tar*\fP) are written to this + directory, separating package output from the transient build + workspace in +TmpDir+. Supports the same variable expansion as + +TmpDir+ (+\fB${USER}\fP+, +\fB${HOME}\fP+, +\fB${UID}\fP+, + +\fB$$\fP+). Defaults to +TmpDir+ when unset or empty. + *AurRepo =* name:: Set the fake AUR repository name. This name should not conflict with names of declared repositories. The default value is +aur+. @@ -56,10 +63,32 @@ Options content of PKGBUILD and other files. Pressing enter key will give the positive answer unless this option is specified. -*PreserveBuilt =* Internal | User | Disabled:: - If set to Internal, built packages will be copied to pacman cache dir. - If set to User, built packages will be copied to UserCacheDir. - If set to Disabled (the default), built packages will not be preserved. +*KeepBuildDirOnFailure*:: + When enabled, pakku preserves the build workspace (cloned repositories + and build directories) when a build fails. By default the workspace is + removed on failure. + +*KeepBuiltPackagesOnFailure*:: + When enabled, pakku preserves built package archives in the staging + directory when a build fails. By default archives are removed on + build failure. + +*PreserveBuilt =* Internal | User | Pkgdest | Disabled:: + Where to copy built packages before invoking pacman. If set to + Internal, packages are copied to the pacman cache directory. If set + to User, packages are copied to +UserCacheDir+. If set to Pkgdest, + packages are copied to the PKGDEST configured in the user's + linkman:makepkg.conf[5]. If set to Disabled (the default), no copy + is performed. + +*CleanupAfterInstall =* Full | Worktree | None:: + Controls cleanup of build artifacts and workspace after a successful + install. Full (the default) removes both temporary build directories + and built package archives. Worktree removes build directories but + preserves package archives selected for installation in the output + directory for reuse on subsequent runs. None preserves the workspace and + selected package archives. Unselected split-package sibling archives are + removed in Worktree and None modes. *PreBuildCommand =* command:: This command will be executed in package directory before building, diff --git a/pakku.conf b/pakku.conf index 11a1c29..bfdeb75 100644 --- a/pakku.conf +++ b/pakku.conf @@ -3,6 +3,7 @@ [options] #UserCacheDir = ${HOME}/.cache/pakku #TmpDir = /tmp/pakku-${USER} +#PackageOutputDir = #AurRepo = aur AurComments @@ -13,7 +14,11 @@ PrintLocalIsNewer #SudoExec #ViewNoDefault -#PreserveBuilt = Disabled +#KeepBuildDirOnFailure +#KeepBuiltPackagesOnFailure + +#PreserveBuilt = Disabled | Internal | User | Pkgdest +#CleanupAfterInstall = Full #PreBuildCommand = #PreferredSudoCommand = diff --git a/src/common.nim b/src/common.nim index 8fa1871..7813922 100644 --- a/src/common.nim +++ b/src/common.nim @@ -91,6 +91,24 @@ proc resolveMakepkgConf*(): tuple[path: Option[Path], error: string] = (none(Path), tr"failed to find makepkg config file") +proc resolveEffectivePkgdest*(confFile: string): string = + ## Sources the resolved makepkg.conf to read the effective PKGDEST value + ## without pakku's override block. Returns "" if PKGDEST is unset (makepkg + ## default: write alongside the PKGBUILD). Environment variable PKGDEST takes + ## precedence over the conf file. + let envDest = getEnv("PKGDEST") + if envDest.len > 0: + return envDest + + forkWaitRedirect(() => (block: + if dropPrivRedirect(): + execRedirect(bashCmd, "-c", + "source \"$@\" && echo \"$PKGDEST\"", + "bash", confFile) + else: + quit(1))) + .output.optFirst.get("") + template checkAndRefreshUpgrade*(sudoPrefix: seq[string], color: bool, args: seq[Argument]): tuple[code: int, args: seq[Argument]] = checkAndRefreshUpgradeInternal(sudoPrefix, color, true, args) diff --git a/src/config.nim b/src/config.nim index b7c18b8..9892b28 100644 --- a/src/config.nim +++ b/src/config.nim @@ -12,54 +12,61 @@ type PreserveBuilt* {.pure.} = enum internal = "Internal", user = "User", + pkgdest = "Pkgdest", disabled = "Disabled" - CommonConfig* = tuple[ - dbs: seq[string], - arch: string, - debug: bool, - progressBar: bool, - chomp: bool, - verbosePkgLists: bool, - downloadTimeout: bool, - pgpKeyserver: Option[string], - defaultRoot: bool, - ignorePkgs: HashSet[string], - ignoreGroups: HashSet[string] - ] - - PacmanConfig* = tuple[ - common: CommonConfig, - sysrootOption: Option[string], - rootRelOption: Option[string], - dbRelOption: Option[string], - cacheRelOption: Option[string], - gpgRelOption: Option[string], - colorMode: ColorMode - ] - - Config* = tuple[ - common: CommonConfig, - root: string, - db: string, - cache: string, - userCacheInitial: string, - userCacheCurrent: string, - tmpRootInitial: string, - tmpRootCurrent: string, - color: bool, - aurRepo: string, - aurComments: bool, - checkIgnored: bool, - ignoreArch: bool, - printAurNotFound: bool, - printLocalIsNewer: bool, - sudoExec: bool, - viewNoDefault: bool, - preserveBuilt: PreserveBuilt, - preBuildCommand: Option[string], - sudoCommand: seq[string] - ] + CleanupPolicy* {.pure.} = enum + full = "Full", + worktree = "Worktree", + none = "None" + + CommonConfig* = object + dbs*: seq[string] + arch*: string + debug*: bool + progressBar*: bool = true + chomp*: bool + verbosePkgLists*: bool + downloadTimeout*: bool = true + pgpKeyserver*: Option[string] + defaultRoot*: bool + ignorePkgs*: HashSet[string] + ignoreGroups*: HashSet[string] + + PacmanConfig* = object + common*: CommonConfig + sysrootOption*: Option[string] + rootRelOption*: Option[string] + dbRelOption*: Option[string] + cacheRelOption*: Option[string] + gpgRelOption*: Option[string] + colorMode*: ColorMode = colorNever + + Config* = object + common*: CommonConfig + root*: string + db*: string + cache*: string + userCacheInitial*: string + userCacheCurrent*: string + tmpRootInitial*: string + tmpRootCurrent*: string + packageOutputDir*: string + color*: bool + aurRepo*: string = "aur" + aurComments*: bool + checkIgnored*: bool + ignoreArch*: bool + printAurNotFound*: bool + printLocalIsNewer*: bool + sudoExec*: bool + viewNoDefault*: bool + keepBuildDirOnFailure*: bool + keepBuiltPackagesOnFailure*: bool + cleanupAfterInstall*: CleanupPolicy = full + preserveBuilt*: PreserveBuilt = disabled + preBuildCommand*: Option[string] + sudoCommand*: seq[string] proc readConfigFile*(configFile: string): (OrderedTable[string, ref Table[string, string]], bool) = @@ -141,6 +148,8 @@ proc extendRel*(pathRel: string, sysroot: Option[string]): string = sysroot.map(s => (s & "/" & pathRel).simplifyConfigPath).get(pathRel) proc obtainConfig*(config: PacmanConfig): Config = + ## Builds the pakku runtime configuration by layering the pacman config with + ## user-specified options from the pakku.conf options section. let (configTable, _) = readConfigFile(SysConfDir & "/pakku.conf") let options = configTable.opt("options").map(t => t[]).get(initTable[string, string]()) @@ -169,18 +178,6 @@ proc obtainConfig*(config: PacmanConfig): Config = tmpRootInitial = obtainTmpDir(initialOrCurrentUser) tmpRootCurrent = obtainTmpDir(currentUser) aurRepo = options.opt("AurRepo").get("aur") - aurComments = options.hasKey("AurComments") - checkIgnored = options.hasKey("CheckIgnored") - ignoreArch = options.hasKey("IgnoreArch") - printAurNotFound = options.hasKey("PrintAurNotFound") - printLocalIsNewer = options.hasKey("PrintLocalIsNewer") - sudoExec = options.hasKey("SudoExec") - viewNoDefault = options.hasKey("ViewNoDefault") - preserveBuilt = toSeq(enumerate[PreserveBuilt]()) - .filter(o => some($o) == options.opt("PreserveBuilt")) - .optLast.get(PreserveBuilt.disabled) - preBuildCommand = options.opt("PreBuildCommand") - sudoCommand = options.opt("PreferredSudoCommand").getSudoPrefix() if config.common.dbs.find(aurRepo) >= 0: raise commandError(tr"repo '$#' can not be used as fake AUR repository" % [aurRepo], @@ -190,13 +187,41 @@ proc obtainConfig*(config: PacmanConfig): Config = raise commandError(trp("could not register '%s' database (%s)\n") % [aurRepo, tra"wrong or NULL argument passed"], colorNeeded = some(color)) - ((config.common.dbs, config.common.arch, config.common.debug, config.common.progressBar, - config.common.chomp, config.common.verbosePkgLists, config.common.downloadTimeout, - config.common.pgpKeyserver, config.common.defaultRoot and config.sysrootOption.isNone, - config.common.ignorePkgs, config.common.ignoreGroups), - root, db, cache, userCacheInitial, userCacheCurrent, tmpRootInitial, tmpRootCurrent, - color, aurRepo, aurComments, checkIgnored, ignoreArch, printAurNotFound, printLocalIsNewer, - sudoExec, viewNoDefault, preserveBuilt, preBuildCommand, sudoCommand) + let common = block: + var c = config.common + c.defaultRoot = c.defaultRoot and config.sysrootOption.isNone + c + + Config( + common: common, + root: root, + db: db, + cache: cache, + userCacheInitial: userCacheInitial, + userCacheCurrent: userCacheCurrent, + tmpRootInitial: tmpRootInitial, + tmpRootCurrent: tmpRootCurrent, + packageOutputDir: options.opt("PackageOutputDir").get("").handleDirPattern(initialOrCurrentUser), + color: color, + aurRepo: aurRepo, + aurComments: options.hasKey("AurComments"), + checkIgnored: options.hasKey("CheckIgnored"), + ignoreArch: options.hasKey("IgnoreArch"), + printAurNotFound: options.hasKey("PrintAurNotFound"), + printLocalIsNewer: options.hasKey("PrintLocalIsNewer"), + sudoExec: options.hasKey("SudoExec"), + viewNoDefault: options.hasKey("ViewNoDefault"), + keepBuildDirOnFailure: options.hasKey("KeepBuildDirOnFailure"), + keepBuiltPackagesOnFailure: options.hasKey("KeepBuiltPackagesOnFailure"), + cleanupAfterInstall: toSeq(enumerate[CleanupPolicy]()) + .filterIt(some($it) == options.opt("CleanupAfterInstall")) + .optLast.get(CleanupPolicy.full), + preserveBuilt: toSeq(enumerate[PreserveBuilt]()) + .filterIt(some($it) == options.opt("PreserveBuilt")) + .optLast.get(PreserveBuilt.disabled), + preBuildCommand: options.opt("PreBuildCommand"), + sudoCommand: options.opt("PreferredSudoCommand").getSudoPrefix(), + ) template withAlpmConfig*(config: Config, passDbs: bool, handle: untyped, alpmDbs: untyped, errors: untyped, body: untyped): untyped = diff --git a/src/feature/syncinstall.nim b/src/feature/syncinstall.nim index 2a014d8..e0d5f6d 100644 --- a/src/feature/syncinstall.nim +++ b/src/feature/syncinstall.nim @@ -1,43 +1,144 @@ import - algorithm, options, os, posix, sequtils, sets, strutils, sugar, tables, - "../args", "../aur", "../config", "../common", "../format", "../lists", "../package", - "../pacman", "../utils", + std/[algorithm, options, os, posix, sequtils, sets, strutils, sugar, tables], + ".."/[args, aur, config, common, format, package, pacman, utils], "../wrapper/alpm" when not declared(system.stdout): import std/syncio +const + PkgExtGlob = ".pkg.tar.*" + type - Installed = tuple[ - name: string, - version: string, - groups: seq[string], - explicit: bool, + Installed = object + name: string + version: string + groups: seq[string] + explicit: bool foreign: bool - ] - SatisfyResult = tuple[ - installed: bool, - name: string, + SatisfyResult = object + installed: bool + name: string buildPkgInfo: Option[PackageInfo] - ] - LocalIsNewer = tuple[ - name: string, - version: string, + LocalIsNewer = object + name: string + version: string aurVersion: string - ] - ReplacePkgInfo = tuple[ - name: Option[string], + ReplacePkgInfo = object + name: Option[string] pkgInfo: PackageInfo - ] - - BuildResult = tuple[ - ext: string, - replacePkgInfos: seq[ReplacePkgInfo] - ] -proc groupsSeq(pkg: ptr AlpmPackage): seq[string] = - toSeq(pkg.groups.items).map(s => $s) + BuildArtifact = object + name: Option[string] + pkgInfo: PackageInfo + file: string + + BuildResult = object + artifacts: seq[BuildArtifact] + + InstallTarget = object + ## A package-to-archive mapping selected for installation. + name: string + file: string + + InstallItem = object + ## Packages to install via the positional subprocess protocol. + ## Field order must stay in sync with the install helper. + name: string + file: string + mode: string + + BuildBasesRes = object + results: seq[BuildResult] + skipped: int + code: int + + InstallArtifacts = object + artifacts: seq[BuildArtifact] ## all built archives including split-package siblings + install: seq[InstallTarget] + installFiles: HashSet[string] ## file paths of install targets; precomputed for O(1) cleanup lookup + builtBases: HashSet[string] + renames: CanonicalRenames + + CanonicalRenames = Table[string, string] + ## Maps artifact names to canonical AUR names for split packages. + ## Only populated for actual renames (artifact.name != rpc.name). + + InstallGroupRes = object + canonicalRenames: CanonicalRenames + code: int + + CleanupSpec = object + ## Post-install cleanup settings. Computed once from + ## CleanupPolicy + context so callers don't re-derive it independently. + removeWorktrees: bool ## remove per-base git clone dirs under tmpRoot + removeArchives: bool ## remove built `PkgExtGlob` files + nukeTmpPrefix: bool ## use removeTmpDirQuiet (chases firstCreatedTmpDir) vs removeDirQuiet + + Upgrade = object + rpcInfo: RpcPackageInfo + needed: bool + + InstallMode {.pure.} = enum + ## Passed verbatim to the install helper subprocess. + ## Values must stay in sync with install.nim. + auto = "auto", + explicit = "explicit", + dependency = "dependency" + + ExecMode = enum + emNormal + emSilent + emRedirect + + AurFetchRes = object ## Result of fetching AUR package infos + ## (clone or RPC-only depending on printMode). + pkgInfos: seq[PackageInfo] + additionalPkgInfos: seq[PackageInfo] + paths: seq[string] + errors: seq[string] + + AurPackageInfosRes = object ## Result of `obtainAurPackageInfos`_ + pkgInfos: seq[PackageInfo] + additionalPkgInfos: seq[PackageInfo] + paths: seq[string] + upToDateNeeded: seq[Installed] + localIsNewerSeq: seq[LocalIsNewer] + errors: seq[string] + + BuildTargetsRes = object ## Result of `resolveBuildTargets`_ + code: int + installed: seq[Installed] + targetNamesSet: HashSet[string] + pkgInfos: seq[PackageInfo] + additionalPkgInfos: seq[PackageInfo] + paths: seq[string] + + PacmanBuildTargetsRes = object ## Result of `obtainPacmanBuildTargets`_ + checked: bool + pkgInfos: seq[PackageInfo] + upToDateNeeded: seq[(string, string)] + paths: seq[string] + errors: seq[string] + + PackageInfoGroup = object + pkgInfos: seq[PackageInfo] + names: HashSet[string] + + ResolvedReference = object + reference: PackageReference + result: SatisfyResult + + IsContainer = concept + proc len(x: Self): int + +template requireNonEmpty(container: IsContainer; routine: static string) = + assert container.len > 0, routine & " requires a non-empty sequence" + +func toSeq(pkgGroups: ptr AlpmList[cstring]): seq[string] = + for s in items(pkgGroups): + result.add $s proc createCloneProgress(config: Config, count: int, flexible: bool, printMode: bool): (proc (update: int, terminate: int) {.closure.}, proc() {.closure.}) = @@ -47,230 +148,306 @@ proc createCloneProgress(config: Config, count: int, flexible: bool, printMode: update(0, count) if flexible: - proc cloneUpdate(progress: int, newCount: int) {.closure.} = + proc cloneUpdate(progress: int; newCount: int) {.closure.} = # newCount can be < count if some packages were not found update(max(count - newCount + progress, 0), count) - (cloneUpdate, terminate) else: (update, terminate) else: (proc (a: int, b: int) {.closure.} = discard, proc {.closure.} = discard) -proc isVcs(name: string): bool = +## Runs body inside a clone-progress display, always calling terminate() +## even if body raises. The injected `updateIdent` name is the progress callback. +## At call sites that need a return value, use `result =` inside the body. +template withCloneProgress(config: Config; count: int; flexible, printMode: bool; + updateIdent, body: untyped): untyped = + let (updateIdent {.inject.}, cloneTerminate) = + createCloneProgress(config, count, flexible, printMode) + try: + body + finally: + cloneTerminate() + +func isVcs(name: string): bool = let index = name.rfind('-') - if index >= 0: - let suffix = name[index + 1 .. ^1] - suffix == "bzr" or suffix == "git" or suffix == "hg" or suffix == "svn" - else: - false - -proc orderInstallation(ordered: seq[seq[seq[PackageInfo]]], grouped: seq[seq[PackageInfo]], - satisfied: Table[PackageReference, SatisfyResult]): seq[seq[seq[PackageInfo]]] = - let orderedNamesSet = collect(initHashSet): - for a in ordered: - for b in a: - for c in b: - {c.rpc.name} + index >= 0 and (let suffix = name[index + 1 .. ^1]; + suffix == "bzr" or suffix == "git" or suffix == "hg" or suffix == "svn") + +func checkSatisfied(handle: ptr AlpmHandle; nodepsCount: int; + reference: PackageReference): bool = + for pkg in handle.local.packages: + if reference.isProvidedBy(pkg.toPackageReference, nodepsCount == 0): + return true + for provides in pkg.provides: + if reference.isProvidedBy(provides.toPackageReference, nodepsCount == 0): + return true + false + +func initPackageInfoGroup(pkgInfos: seq[PackageInfo]): PackageInfoGroup = + result.pkgInfos = pkgInfos + for pkgInfo in pkgInfos: + result.names.incl pkgInfo.rpc.name - proc hasBuildDependency(pkgInfos: seq[PackageInfo]): bool = - for pkgInfo in pkgInfos: - for reference in pkgInfo.allDepends: - for satres in satisfied.opt(reference): - if satres.buildPkgInfo.isSome and - not (satres.buildPkgInfo.unsafeGet in pkgInfos) and - not (satres.buildPkgInfo.unsafeGet.rpc.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, satisfied) - else: - newOrdered - -proc orderInstallation(pkgInfos: seq[PackageInfo], - satisfied: Table[PackageReference, SatisfyResult]): seq[seq[seq[PackageInfo]]] = - let grouped = pkgInfos.groupBy(i => i.rpc.base).map(p => p.values) - - orderInstallation(@[], grouped, satisfied) - .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], - totalAurFail: seq[PackageReference], additionalPkgInfos: seq[PackageInfo], paths: seq[string], - nodepsCount: int, assumeInstalled: seq[PackageReference], printMode: bool, noaur: bool): - (Table[PackageReference, SatisfyResult], seq[PackageReference], seq[string]) = - proc checkDependencyCycle(pkgInfo: PackageInfo, reference: PackageReference): bool = - for checkReference in pkgInfo.allDepends: - if checkReference == reference: - return false - let buildPkgInfo = satisfied.opt(checkReference) - .map(r => r.buildPkgInfo).flatten - if buildPkgInfo.isSome and not checkDependencyCycle(buildPkgInfo.unsafeGet, reference): - return false - return true - - 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 +func hasBuildDependency(satisfied: Table[PackageReference, SatisfyResult]; + orderedNames, groupNames: HashSet[string]; pkgInfos: seq[PackageInfo]): bool = + for pkgInfo in pkgInfos: + for reference in pkgInfo.allDepends: + for satres in satisfied.opt(reference): + if satres.buildPkgInfo.isSome and + satres.buildPkgInfo.unsafeGet.rpc.name notin groupNames and + satres.buildPkgInfo.unsafeGet.rpc.name notin orderedNames: + return true + false + +proc orderInstallation(pkgInfos: seq[PackageInfo]; + satisfied: Table[PackageReference, SatisfyResult]): seq[seq[seq[PackageInfo]]] = + ## Builds dependency layers by package base. Each pass emits bases whose + ## build-time dependencies are already emitted or live in the same base. + var remaining = pkgInfos.groupBy(i => i.rpc.base).mapIt(it.values.initPackageInfoGroup) + var orderedNames = initHashSet[string]() + + while remaining.len > 0: + var layer: seq[seq[PackageInfo]] + var next: seq[PackageInfoGroup] + + for group in remaining: + if hasBuildDependency(satisfied, orderedNames, group.names, group.pkgInfos): + next.add group + elif group.pkgInfos.len > 0: + layer.add group.pkgInfos + + if layer.len == 0: + var cycleLayer: seq[seq[PackageInfo]] + for group in next: + if group.pkgInfos.len > 0: + cycleLayer.add group.pkgInfos + if cycleLayer.len > 0: + result.add cycleLayer + break + + result.add layer + for group in layer: + for pkgInfo in group: + orderedNames.incl pkgInfo.rpc.name + remaining = next + +func checkDependencyCycle(satisfied: Table[PackageReference, SatisfyResult]; + pkgInfo: PackageInfo; reference: PackageReference): bool = + for checkReference in pkgInfo.allDepends: + if checkReference == reference: + return false + let buildPkgInfo = satisfied.opt(checkReference).map(r => r.buildPkgInfo).flatten + if buildPkgInfo.isSome and not checkDependencyCycle(satisfied, buildPkgInfo.unsafeGet, reference): + return false + true + +func findInSatisfied(satisfied: Table[PackageReference, SatisfyResult]; + reference: PackageReference; nodepsCount: int): Option[PackageInfo] = + for satref, res in satisfied.pairs: + if res.buildPkgInfo.isSome: + let pkgInfo = res.buildPkgInfo.unsafeGet + if satref == reference or reference .isProvidedBy(pkgInfo.rpc.toPackageReference, nodepsCount == 0): - return some(pkgInfo) - for provides in pkgInfo.provides: - if reference.isProvidedBy(provides, nodepsCount == 0) and - checkDependencyCycle(pkgInfo, reference): - return some(pkgInfo) - return none(PackageInfo) - - proc findInAdditional(reference: PackageReference): Option[PackageInfo] = - for pkgInfo in additionalPkgInfos: - if reference.isProvidedBy(pkgInfo.rpc.toPackageReference, nodepsCount == 0): return some(pkgInfo) for provides in pkgInfo.provides: if reference.isProvidedBy(provides, nodepsCount == 0) and - checkDependencyCycle(pkgInfo, reference): + checkDependencyCycle(satisfied, pkgInfo, 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.toPackageReference, nodepsCount == 0): - return some(($pkg.name, pkg.groupsSeq)) - for provides in pkg.provides: - if reference.isProvidedBy(provides.toPackageReference, nodepsCount == 0): - 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(PackageInfo) + +func findInAdditional(additionalPkgInfos: seq[PackageInfo]; + satisfied: Table[PackageReference, SatisfyResult]; + reference: PackageReference; nodepsCount: int): Option[PackageInfo] = + for pkgInfo in additionalPkgInfos: + if reference.isProvidedBy(pkgInfo.rpc.toPackageReference, nodepsCount == 0): + return some(pkgInfo) + for provides in pkgInfo.provides: + if reference.isProvidedBy(provides, nodepsCount == 0) and + checkDependencyCycle(satisfied, pkgInfo, reference): + return some(pkgInfo) + none(PackageInfo) + +func findInDatabaseWithGroups(db: ptr AlpmDatabase; reference: PackageReference; + directName: bool; nodepsCount: int): Option[tuple[name: string, groups: seq[string]]] = + for pkg in db.packages: + if reference.isProvidedBy(pkg.toPackageReference, nodepsCount == 0): + return some(($pkg.name, pkg.groups.toSeq())) + for provides in pkg.provides: + if reference.isProvidedBy(provides.toPackageReference, nodepsCount == 0): + return some(((if directName: $pkg.name else: $provides.name), pkg.groups.toSeq())) + none((string, seq[string])) + +func findInDatabase(config: Config; db: ptr AlpmDatabase; reference: PackageReference; + directName, checkIgnored: bool; nodepsCount: int): Option[string] = + let res = findInDatabaseWithGroups(db, reference, directName, nodepsCount) + if res.isSome: + let r = res.unsafeGet + if checkIgnored and config.ignored(r.name, r.groups): 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: - if nodepsCount >= 2 or - assumeInstalled.filter(r => reference.isProvidedBy(r, true)).len > 0: - some((true, reference.name, none(PackageInfo))) + some(r.name) + else: + none(string) + +func findInDatabases(config: Config; dbs: seq[ptr AlpmDatabase]; reference: PackageReference; + directName, checkIgnored: bool; nodepsCount: int): Option[string] = + for db in dbs: + let name = findInDatabase(config, db, reference, directName, checkIgnored, nodepsCount) + if name.isSome: + return name + none(string) + +func resolveReference(config: Config; handle: ptr AlpmHandle; + dbs: seq[ptr AlpmDatabase]; satisfied: Table[PackageReference, SatisfyResult]; + additionalPkgInfos: seq[PackageInfo]; nodepsCount: int; + assumeInstalled: seq[PackageReference]; + reference: PackageReference): Option[SatisfyResult] = + let localName = findInDatabase(config, handle.local, reference, true, false, nodepsCount) + if localName.isSome: + return some(SatisfyResult(installed: true, name: localName.unsafeGet, + buildPkgInfo: none(PackageInfo))) + + if nodepsCount >= 2 or assumeInstalled.anyIt(reference.isProvidedBy(it, true)): + return some(SatisfyResult(installed: true, name: reference.name, + buildPkgInfo: none(PackageInfo))) + + let pkgInfo = findInSatisfied(satisfied, reference, nodepsCount) + if pkgInfo.isSome: + return some(SatisfyResult(installed: false, name: pkgInfo.unsafeGet.rpc.name, + buildPkgInfo: pkgInfo)) + + let addlPkgInfo = findInAdditional(additionalPkgInfos, satisfied, reference, nodepsCount) + if addlPkgInfo.isSome: + return some(SatisfyResult(installed: false, name: addlPkgInfo.unsafeGet.rpc.name, + buildPkgInfo: addlPkgInfo)) + + let syncName = findInDatabases(config, dbs, reference, false, true, nodepsCount) + if syncName.isSome: + return some(SatisfyResult(installed: false, name: syncName.unsafeGet, + buildPkgInfo: none(PackageInfo))) + + none(SatisfyResult) + +proc rpcNames(rpcs: openArray[RpcPackageInfo]): seq[string] = + ## Extracts bare package names from a slice of RPC info records. + result = newSeqOfCap[string](rpcs.len) + for r in rpcs: + result.add r.name + +proc fetchAurInfos(config: Config; names: seq[string]; + printMode: bool; update: (int, int) -> void): AurFetchRes = + ## Fetches AUR package infos, branching on printMode: + ## - print mode: RPC + HTTP .SRCINFO only (no git clone). + ## - install mode: clone bare AUR repos into the temp workspace. + ## The update callback receives (progress, total) for the progress bar. + if printMode: + let (pkgInfos, additionalPkgInfos, aerrors) = getAurPackageInfos(names, + config.aurRepo, config.common.arch, config.common.downloadTimeout, config.color) + AurFetchRes( + pkgInfos: pkgInfos, + additionalPkgInfos: additionalPkgInfos, + errors: aerrors.deduplicate) + else: + let (rpcInfos, aerrors) = getRpcPackageInfos(names, + config.aurRepo, config.common.downloadTimeout, config.color) + let (pkgInfos, additionalPkgInfos, paths, cerrors) = + cloneAurReposWithPackageInfos(config, rpcInfos, true, update, true) + var errors: seq[string] + for e in aerrors: errors.add e + for e in cerrors: errors.add e + AurFetchRes( + pkgInfos: pkgInfos, + additionalPkgInfos: additionalPkgInfos, + paths: paths, + errors: errors.deduplicate) + +proc findDependencies(config: Config; handle: ptr AlpmHandle; dbs: seq[ptr AlpmDatabase]; + satisfied: Table[PackageReference, SatisfyResult]; unsatisfied: seq[PackageReference]; + totalAurFail: seq[PackageReference]; additionalPkgInfos: seq[PackageInfo]; + paths: seq[string]; nodepsCount: int; assumeInstalled: seq[PackageReference]; + printMode, noaur: bool): + (Table[PackageReference, SatisfyResult], seq[PackageReference], seq[string]) = + var currentSatisfied = satisfied + var currentUnsatisfied = unsatisfied + var currentAurFail = totalAurFail + var currentAdditionalPkgInfos = additionalPkgInfos + var currentPaths = paths + + while true: + let (success, aurCheck) = block: + var resolved: seq[ResolvedReference] + var check: seq[PackageReference] + + for r in currentUnsatisfied: + let res = resolveReference(config, handle, dbs, currentSatisfied, + currentAdditionalPkgInfos, nodepsCount, assumeInstalled, r) + if res.isSome: + resolved.add ResolvedReference(reference: r, result: res.unsafeGet) + elif r notin currentAurFail: + check.add r + (resolved, check) + + let (aurSuccess, aurFail, newPaths, newAdditionalPkgInfos) = block: + var resolved: seq[ResolvedReference] + var failed: seq[PackageReference] + var fetchedPaths: seq[string] + var fetchedAdditional: seq[PackageInfo] + + if not noaur and aurCheck.len > 0: + withCloneProgress(config, aurCheck.len, true, printMode, cloneUpdate): + withAur(): + let fetched = fetchAurInfos(config, aurCheck.mapIt(it.name), printMode, cloneUpdate) + for e in fetched.errors: printError(config.color, e) + + let nameTable = block: + var table = initTable[string, PackageInfo]() + for i in fetched.pkgInfos: + if not config.ignored(i.rpc.name, i.groups): + table[i.rpc.name] = i + table + for r in aurCheck: + if r.name in nameTable: + resolved.add ResolvedReference(reference: r, + result: SatisfyResult(installed: false, name: r.name, + buildPkgInfo: some(nameTable[r.name]))) + else: + failed.add r + fetchedPaths = fetched.paths + fetchedAdditional = fetched.additionalPkgInfos else: - let pkgInfo = findInSatisfied(reference) - if pkgInfo.isSome: - some((false, pkgInfo.unsafeGet.rpc.name, pkgInfo)) - else: - let pkgInfo = findInAdditional(reference) - if pkgInfo.isSome: - some((false, pkgInfo.unsafeGet.rpc.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) - .filter(r => not (r in totalAurFail)) - - let (aurSuccess, aurFail, newPaths, newAdditionalPkgInfos) = - if not noaur and aurCheck.len > 0: (block: - let (update, terminate) = createCloneProgress(config, aurCheck.len, true, printMode) - try: - withAur(): - let (pkgInfos, additionalPkgInfos, paths) = if printMode: (block: - let (pkgInfos, additionalPkgInfos, aerrors) = getAurPackageInfos(aurCheck - .map(r => r.name), config.aurRepo, config.common.arch, - config.common.downloadTimeout, config.color) - for e in aerrors: printError(config.color, e) - (pkgInfos, additionalPkgInfos, newSeq[string]())) - else: (block: - let (rpcInfos, aerrors) = getRpcPackageInfos(aurCheck.map(r => r.name), - config.aurRepo, config.common.downloadTimeout, config.color) - for e in aerrors: printError(config.color, e) - let (pkgInfos, additionalPkgInfos, paths, cerrors) = - cloneAurReposWithPackageInfos(config, rpcInfos, not printMode, update, true) - for e in cerrors: printError(config.color, e) - (pkgInfos, additionalPkgInfos, paths)) - - let acceptedPkgInfos = pkgInfos.filter(i => not config.ignored(i.rpc.name, i.groups)) - let aurTable = acceptedPkgInfos.map(i => (i.rpc.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, paths, additionalPkgInfos) - 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 = deduplicate: - collect(newSeq): - for y in aurSuccess: - for r in y.result: - for i in r.buildPkgInfo: + failed = aurCheck + (resolved, failed, fetchedPaths, fetchedAdditional) + + for sr in success: + currentSatisfied[sr.reference] = sr.result + for ar in aurSuccess: + currentSatisfied[ar.reference] = ar.result + + currentPaths.add newPaths + currentAdditionalPkgInfos.add newAdditionalPkgInfos + currentAurFail = (currentAurFail & aurFail).deduplicate + + let newUnsatisfied = deduplicate: + collect(newSeq): + for y in aurSuccess: + for i in y.result.buildPkgInfo: for x in i.allDepends: x - let newTotalAurFail = (totalAurFail & aurFail).deduplicate - let newTotalUnsatisfied = (newUnsatisfied & newTotalAurFail).deduplicate + if newUnsatisfied.len == 0: + let finallyUnsatisfied = currentAurFail.filterIt(it notin currentSatisfied) + return (currentSatisfied, finallyUnsatisfied, currentPaths) - if newUnsatisfied.len > 0: - findDependencies(config, handle, dbs, newSatisfied, newTotalUnsatisfied, newTotalAurFail, - additionalPkgInfos & newAdditionalPkgInfos, paths & newPaths, - nodepsCount, assumeInstalled, printMode, noaur) - else: - let finallyUnsatisfied = newTotalAurFail.filter(r => not newSatisfied.hasKey(r)) - (newSatisfied, finallyUnsatisfied, paths & newPaths) - -proc findDependencies(config: Config, handle: ptr AlpmHandle, - dbs: seq[ptr AlpmDatabase], pkgInfos: seq[PackageInfo], additionalPkgInfos: seq[PackageInfo], - nodepsCount: int, assumeInstalled: seq[PackageReference], printMode: bool, noaur: bool): - (Table[PackageReference, SatisfyResult], seq[PackageReference], seq[string]) = - let satisfied = pkgInfos.map(p => ((p.rpc.name, none(string), none(VersionConstraint)), - (false, p.rpc.name, some(p)))).toTable + currentUnsatisfied = (newUnsatisfied & currentAurFail).deduplicate + +proc findDependencies(config: Config; handle: ptr AlpmHandle; + dbs: seq[ptr AlpmDatabase]; pkgInfos: seq[PackageInfo]; + additionalPkgInfos: seq[PackageInfo]; nodepsCount: int; + assumeInstalled: seq[PackageReference]; printMode, noaur: bool): + (Table[PackageReference, SatisfyResult], seq[PackageReference], seq[string]) = + let satisfied = pkgInfos.mapIt(((it.rpc.name, none(string), none(VersionConstraint)), + SatisfyResult(installed: false, name: it.rpc.name, buildPkgInfo: some(it)))).toTable let unsatisfied = deduplicate: collect(newSeq): for i in pkgInfos: @@ -279,16 +456,60 @@ proc findDependencies(config: Config, handle: ptr AlpmHandle, findDependencies(config, handle, dbs, satisfied, unsatisfied, @[], additionalPkgInfos, @[], nodepsCount, assumeInstalled, printMode, noaur) -template clearPaths(paths: untyped, tmpDir: bool = false) = +proc clearWorktrees(config: Config; paths: openArray[string]; spec: CleanupSpec) = + ## Removes per-base git clone working trees according to spec. + ## nukeTmpPrefix drives removeTmpDirQuiet (chases firstCreatedTmpDir up the tree) + ## vs plain removeDirQuiet (removes only the named directory). + if not spec.removeWorktrees: return for path in paths: - if tmpDir: - removeTmpDirQuiet(path) - else: - removeDirQuiet(path) + if spec.nukeTmpPrefix: removeTmpDirQuiet(path) + else: removeDirQuiet(path) discard rmdir(cstring(config.tmpRootInitial)) -proc printUnsatisfied(config: Config, - satisfied: Table[PackageReference, SatisfyResult], unsatisfied: seq[PackageReference]) = +func cleanupSpec(policy: CleanupPolicy; archivesOutsideTmp: bool): CleanupSpec = + ## Derives cleanup behaviour from post-install policy. + ## archivesOutsideTmp must be true when packageOutputDir is set — only then + ## is it safe to nuke the tmpRoot prefix without losing package archives. + case policy + of CleanupPolicy.full: + CleanupSpec(removeWorktrees: true, removeArchives: true, nukeTmpPrefix: true) + of CleanupPolicy.worktree: + CleanupSpec(removeWorktrees: true, removeArchives: false, nukeTmpPrefix: archivesOutsideTmp) + of CleanupPolicy.none: + CleanupSpec(removeWorktrees: false, removeArchives: false, nukeTmpPrefix: false) + +type FailureKind = enum + buildFailure + installFailure + +func cleanupSpecForFailure(kind: FailureKind; config: Config; + archivesOutsideTmp: bool): CleanupSpec = + case kind + of buildFailure: + CleanupSpec( + removeWorktrees: not config.keepBuildDirOnFailure, + removeArchives: not config.keepBuiltPackagesOnFailure, + nukeTmpPrefix: not config.keepBuildDirOnFailure and archivesOutsideTmp) + of installFailure: + CleanupSpec( + removeWorktrees: false, + removeArchives: false, + nukeTmpPrefix: false) + +proc cleanupArtifacts(config: Config; artifacts: InstallArtifacts; + savedTo: string; spec: CleanupSpec) = + ## Removes or retains built package archives according to spec. + ## Non-install-target files (e.g. split-package siblings) are always removed. + for a in artifacts.artifacts: + if spec.removeArchives or a.file notin artifacts.installFiles: + try: removeFile(a.file) + except CatchableError: discard + if not spec.removeArchives and savedTo.len > 0: + printWarning(config.color, tr"packages are saved to '$#'" % [savedTo]) + +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: @@ -298,936 +519,1095 @@ proc printUnsatisfied(config: Config, trp("unable to satisfy dependency '%s' required by %s\n") % [$reference, pkgInfo.rpc.name]) -template dropPrivilegesAndChdir(path: Option[string], body: untyped): int = - if dropPrivileges(): - if path.isNone or chdir(cstring(path.unsafeGet)) == 0: - body - else: - printError(config.color, tr"chdir failed: $#" % [path.unsafeGet]) +template createViewTag(repo: string; base: string): string = + "view-" & repo & "/" & base + +func targetArguments(names: openArray[string]): seq[Argument] = + result = newSeqOfCap[Argument](names.len) + for name in names: + result.add (name, none(string), ArgumentType.target) + +func targetArguments(names: HashSet[string]): seq[Argument] = + result = newSeqOfCap[Argument](names.len) + for name in names.items: + result.add (name, none(string), ArgumentType.target) + +proc exec( + color: bool; + child: proc(): int {.closure.}; + cwd: Option[string] = none(string); + mode: ExecMode = emNormal; + dropPrivs = false + ): tuple[output: seq[string], code: int] = + ## Execute child workload in isolated subprocess with optional: + ## - privilege dropping + ## - cwd change + ## - output capture + ## - stdio suppression + result = (output: @[], code: -1) + + proc wrapped(): int = + if dropPrivs: + let dropOk = if mode == emRedirect: dropPrivRedirect() else: dropPrivileges() + if not dropOk: + printError(color, tr"failed to drop privileges") + quit(1) + if cwd.isSome and chdir(cstring(cwd.unsafeGet)) != 0: + printError(color, tr"chdir failed: $#" % [cwd.unsafeGet]) quit(1) + if mode == emSilent: + discard close(0); + discard open("/dev/null") + discard close(1); + discard open("/dev/null") + discard close(2); + discard open("/dev/null") + child() + + if mode == emRedirect: + result = forkWaitRedirect(wrapped) else: - printError(config.color, tr"failed to drop privileges") - quit(1) - -template dropPrivRedirectAndChdir(path: Option[string], body: untyped): int = - if dropPrivRedirect(): - if path.isNone or chdir(cstring(path.unsafeGet)) == 0: - body + result.code = forkWait(wrapped) + +proc exec(color: bool; + args: openArray[string]; + cwd: Option[string] = none(string); + mode: ExecMode = emNormal; + dropPrivs = false + ): tuple[output: seq[string], code: int] = + ## Execute a command with optional privilege drop, output redirection, + ## and working directory change. Returns captured output (if redirected) and exit code. + let argv = @args + exec(color, proc(): int = + if mode == emRedirect: execRedirect(argv) + else: execResult(argv), cwd, mode, dropPrivs) + +func pkgArch(config: Config; pkgInfo: PackageInfo): string = + ## `parseSrcInfo` omits `any` from archs, leaving an empty list for noarch + ## packages. + if pkgInfo.archs.len == 0: "any" else: config.common.arch + +func pkgFileName(pkgInfo: PackageInfo; arch: string): string = + pkgInfo.rpc.name & "-" & pkgInfo.rpc.version & "-" & arch + +func artifactStem(config: Config; pkgInfo: PackageInfo): string = + pkgFileName(pkgInfo, pkgArch(config, pkgInfo)) + +func artifactStemFromFilename(filename, extGlob: string): Option[string] = + if extGlob == PkgExtGlob: + let idx = filename.find(".pkg.tar.") + let suffixStart = idx + ".pkg.tar.".len + if idx > 0 and suffixStart < filename.len and + '.' notin filename[suffixStart .. ^1]: + some(filename[0 ..< idx]) else: - printError(config.color, tr"chdir failed: $#" % [path.unsafeGet]) - quit(1) + none(string) + elif filename.endsWith(extGlob): + some(filename[0 ..< filename.len - extGlob.len]) else: - printError(config.color, tr"failed to drop privileges") - quit(1) - -template createViewTag(repo: string, base: string): string = - "view-" & repo & "/" & base + none(string) + +proc findArtifactsInDir(config: Config; dir: string; + replacePkgInfos: openArray[ReplacePkgInfo]; + reportErrors: bool; extGlob: string): seq[BuildArtifact] = + ## Scans dir once for packages matching name-version-arch. + ## Returns empty seq on first miss (with optional error print). + if dir.len == 0: return @[] + + var expected = initHashSet[string]() + for ri in replacePkgInfos: + expected.incl artifactStem(config, ri.pkgInfo) + + var fileTable = initTable[string, string]() + for (kind, path) in walkDir(dir): + if kind == pcFile: + for stem in artifactStemFromFilename(path.extractFilename, extGlob): + if stem in expected and stem notin fileTable: + fileTable[stem] = path + + var artifacts = newSeq[BuildArtifact]() + for ri in replacePkgInfos: + let pkgInfo = ri.pkgInfo + let file = fileTable.opt(artifactStem(config, pkgInfo)) + if file.isSome: + artifacts.add BuildArtifact(name: ri.name, pkgInfo: pkgInfo, file: file.unsafeGet) + else: + if reportErrors: + printError(config.color, tr"$#: failed to find built package archive" % + [pkgInfo.rpc.name]) + return @[] + artifacts + +proc findReusableArtifacts(config: Config; pkgInfos: seq[PackageInfo]; + outputDir, effectivePkgdest: string): seq[BuildArtifact] = + ## Cross-run reuse: scans outputDir then PreserveBuilt destinations. + ## Returns first match across all directories. + let replacePkgInfos = pkgInfos.mapIt(ReplacePkgInfo(name: some(it.rpc.name), pkgInfo: it)) + var dirs = @[outputDir] + case config.preserveBuilt: + of PreserveBuilt.pkgdest: + if effectivePkgdest.len > 0: dirs.add effectivePkgdest + of PreserveBuilt.user: + let dir = config.userCacheInitial.cache(CacheKind.packages) + if dir.len > 0: dirs.add dir + of PreserveBuilt.internal: + if config.cache.len > 0: dirs.add config.cache + else: discard + for dir in dirs: + let found = findArtifactsInDir(config, dir, replacePkgInfos, + reportErrors = false, extGlob = PkgExtGlob) + if found.len > 0: return found + @[] + +func isValidPackagesUrl(url: string): bool = + url.startsWith("https://github.com/archlinux/") or + url == "https://gitea.artixlinux.org/packages" + +proc editFileLoop(config: Config; base, repoPath: string; + gitSubdir: Option[string]; default: char; noconfirm: bool; + file: string): char = + let res = printColonUserChoiceWithHelp(config.color, + tr"View and edit $#?" % [base / file], + choices('y', 'n', ('s', tr"skip all"), ('a', tr"abort operation")), + default, noconfirm, 'n') + + if res != 'y': + return res + + let visualEnv = getEnv("VISUAL") + let editorEnv = getEnv("EDITOR") + let editor = if visualEnv.len > 0: visualEnv + elif editorEnv.len > 0: editorEnv + else: + printColonUserInput(config.color, tr"Enter editor executable name" & ":", + noconfirm, "", "") + + if editor.strip.len == 0: + return 'n' + + let buildPath = buildPath(repoPath, gitSubdir) + discard exec(config.color, [bashCmd, "-c", """$1 "$2"""", "bash", editor, file], + some(buildPath), emNormal, true) + editFileLoop(config, base, repoPath, gitSubdir, default, noconfirm, file) + +proc editFileLoopAll(config: Config; base, repoPath: string; + gitSubdir: Option[string]; default: char; noconfirm: bool; + files: openArray[string]): char = + ## Offers interactive edit for each file in sequence; returns the last + ## non-'n' user response (e.g. 's' to skip all, 'a' to abort), or 'n'. + for file in files: + let res = editFileLoop(config, base, repoPath, gitSubdir, default, noconfirm, file) + if res != 'n': + return res + 'n' + +proc viewDiffLoop(config: Config; base, repoPath: string; + gitSubdir: Option[string]; default: char; noconfirm: bool; + tag: string; files: openArray[string]; hasChanges: bool): char = + let res = if hasChanges: + printColonUserChoiceWithHelp(config.color, + tr"View changes in $#?" % [base], + choices('y', 'n', ('e', tr"edit files"), ('s', tr"skip all"), ('a', tr"abort operation")), + default, noconfirm, 'n') + else: + printColonUserChoiceWithHelp(config.color, + tr"No changes in $#. Edit files?" % [base], + choices('y', 'n', ('s', tr"skip all"), ('a', tr"abort operation")), + 'n', noconfirm, 'n') + + if hasChanges and res == 'y': + discard exec(config.color, [gitCmd, "-C", repoPath, "diff", tag & "..@", gitSubdir.get(".")], + none(string), emNormal, true) + viewDiffLoop(config, base, repoPath, gitSubdir, default, noconfirm, tag, files, hasChanges) + elif (hasChanges and res == 'e') or (not hasChanges and res == 'y'): + editFileLoopAll(config, base, repoPath, gitSubdir, default, noconfirm, files) + else: + res -proc editLoop(config: Config, repo: string, base: string, repoPath: string, - gitSubdir: Option[string], defaultYes: bool, noconfirm: bool, trunkPath: bool): char = +proc editLoop(config: Config; repo, base, repoPath: string; + gitSubdir: Option[string]; defaultYes, noconfirm, trunkPath: bool): char = let default = if defaultYes: 'y' else: 'n' - proc editFileLoop(file: string): char = - let res = printColonUserChoiceWithHelp(config.color, - tr"View and edit $#?" % [base & "/" & file], - choices('y', 'n', ('s', tr"skip all"), ('a', tr"abort operation")), - default, noconfirm, 'n') - - if res == 'y': - let visualEnv = getEnv("VISUAL") - let editorEnv = getEnv("EDITOR") - let editor = if visualEnv.len > 0: - visualEnv - elif editorEnv.len > 0: - editorEnv - else: - printColonUserInput(config.color, tr"Enter editor executable name" & ":", - noconfirm, "", "") - - if editor.strip.len == 0: - 'n' - else: - discard forkWait(() => (block: - let buildPath = buildPath(repoPath, gitSubdir) - dropPrivilegesAndChdir(some(buildPath)): - execResult(bashCmd, "-c", """$1 "$2"""", "bash", editor, file))) - editFileLoop(file) - else: - res - let rawFiles = getGitFiles(repoPath, gitSubdir, true, trunkPath) - var files = ("PKGBUILD" & rawFiles.filter(x => x != ".SRCINFO")).deduplicate - if trunkPath == true: + var files = ("PKGBUILD" & rawFiles.filterIt(it != ".SRCINFO")).deduplicate + if trunkPath: for i in 0 ..< files.len: - files[i] = "trunk/" & files[i] - - proc editFileLoopAll(index: int): char = - if index < files.len: - let res = editFileLoop(files[index]) - if res == 'n': editFileLoopAll(index + 1) else: res - else: - 'n' + files[i] = "trunk" / files[i] let tag = createViewTag(repo, base) - proc viewDiffLoop(hasChanges: bool): char = - let res = if hasChanges: - printColonUserChoiceWithHelp(config.color, - tr"View changes in $#?" % [base], - choices('y', 'n', ('e', tr"edit files"), ('s', tr"skip all"), ('a', tr"abort operation")), - default, noconfirm, 'n') - else: - printColonUserChoiceWithHelp(config.color, - tr"No changes in $#. Edit files?" % [base], - choices('y', 'n', ('s', tr"skip all"), ('a', tr"abort operation")), - 'n', noconfirm, 'n') - - if hasChanges and res == 'y': - discard forkWait(() => (block: - dropPrivilegesAndChdir(none(string)): - execResult(gitCmd, "-C", repoPath, "diff", tag & "..@", gitSubdir.get(".")))) - viewDiffLoop(hasChanges) - elif (hasChanges and res == 'e') or (not hasChanges and res == 'y'): - editFileLoopAll(0) - else: - res - - let (hasChanges, noTag) = if repo == config.aurRepo: (block: - let revisions = forkWaitRedirect(() => (block: - dropPrivRedirectAndChdir(none(string)): - execRedirect(gitCmd, "-C", repoPath, "rev-list", tag & "..@"))) - + let (hasChanges, noTag) = if repo == config.aurRepo: + let revisions = exec(config.color, [gitCmd, "-C", repoPath, "rev-list", tag & "..@"], + none(string), emRedirect, true) if revisions.code != 0: (false, true) elif revisions.output.len == 0: (false, false) - else: (block: - let diff = forkWaitRedirect(() => (block: - dropPrivRedirectAndChdir(none(string)): - execRedirect(gitCmd, "-C", repoPath, "diff", tag & "..@", gitSubdir.get(".")))) - (diff.output.len > 0, false))) + else: + let diff = exec(config.color, + [gitCmd, "-C", repoPath, "diff", tag & "..@", gitSubdir.get(".")], + none(string), emRedirect, true) + (diff.output.len > 0, false) else: (false, true) if noTag: - editFileLoopAll(0) + editFileLoopAll(config, base, repoPath, gitSubdir, default, noconfirm, files) else: - viewDiffLoop(hasChanges) + viewDiffLoop(config, base, repoPath, gitSubdir, default, noconfirm, tag, files, hasChanges) + +proc keysLoop(config: Config; pgpKeys: seq[string]; noconfirm: bool): char = + ## Import missing PGP keys for packages about to be built. + ## Returns 'a' if the user aborts, otherwise 'n'. + var skipAll = false + for pgpKey in pgpKeys: + if exec(config.color, [gpgCmd, "--list-keys", pgpKey], + none(string), emSilent, true).code == 0: + continue # key already present + + # Retry importing this key until it succeeds, the user skips/aborts, + # or noconfirm/skipAll is in effect (which skips silently on failure). + while true: + let res = if skipAll: 'y' + else: + printColonUserChoiceWithHelp(config.color, + tr"Import PGP key $#?" % [pgpKey], + choices('y', 'n', ('c', tr"import all keys"), ('a', tr"abort operation")), + 'y', noconfirm, 'y') + + if res == 'a': + return 'a' + if res == 'n': + break # skip this key, move to next + + if res == 'c': + skipAll = true + + # res == 'y' or skipAll: attempt import + let importCode = + if config.common.pgpKeyserver.isSome: + exec(config.color, [gpgCmd, + "--keyserver", config.common.pgpKeyserver.unsafeGet, + "--recv-keys", pgpKey], none(string), emNormal, true).code + else: + exec(config.color, [gpgCmd, "--recv-keys", pgpKey], + none(string), emNormal, true).code + + if importCode == 0 or skipAll or noconfirm: + break # success or non-interactive: move on regardless + # Import failed interactively: print error and re-ask for the same key. + echo(tr"Error - gpg return code = ", importCode) + 'n' + +proc checkNext(config: Config; flatBasePackages: openArray[seq[PackageInfo]]; + noconfirm: bool): int = + ## Reviews each package base interactively (diff, edit, PGP keys). + ## Returns 0 on success, 1 if the user aborts. + var skipEdit = false + for pkgInfos in flatBasePackages: + requireNonEmpty(pkgInfos, "checkNext") + let repo = pkgInfos[0].rpc.repo + let base = pkgInfos[0].rpc.base + let rpath = repoPath(config.tmpRootInitial, base) + let isTrunkPath = pkgInfos[0].rpc.gitUrl.isValidPackagesUrl() + let aur = repo == config.aurRepo + + if not skipEdit and aur and not noconfirm 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: + printComments(config.color, pkgInfos[0].rpc.maintainer, toSeq(comments.reversed)) + + let editRes = if skipEdit or noconfirm: 'n' + else: + editLoop(config, repo, base, rpath, pkgInfos[0].rpc.gitSubdir, + aur and not config.viewNoDefault, noconfirm, isTrunkPath) -proc buildLoop(config: Config, pkgInfos: seq[PackageInfo], skipDeps: bool, - noconfirm: bool, noextract: bool): (Option[BuildResult], int, bool) = - let base = pkgInfos[0].rpc.base - let repoPath = repoPath(config.tmpRootInitial, base) - let gitSubdir = pkgInfos[0].rpc.gitSubdir - var buildPath = buildPath(repoPath, gitSubdir) - if contains(pkgInfos[0].rpc.gitUrl, "https://github.com/archlinux/") or pkgInfos[0].rpc.gitUrl == "https://gitea.artixlinux.org/packages": - buildPath = buildPath & "trunk/" + if editRes == 'a': + return 1 - let (confFileOpt, confError) = resolveMakepkgConf() - if confFileOpt.isNone and confError.len > 0: - printError(config.color, confError) - return (none(BuildResult), 1, false) - let confFile = confFileOpt.unsafeGet.string + let resultPkgInfos = + if isTrunkPath: reloadPkgInfos(config, rpath / "trunk/", pkgInfos) + else: reloadPkgInfos(config, rpath / pkgInfos[0].rpc.gitSubdir.get("."), pkgInfos) + + let pgpKeys = deduplicate: + collect(newSeq): + for p in resultPkgInfos: + for x in p.pgpKeys: + x - let workConfFile = config.tmpRootInitial & "/makepkg.conf" + let keysRes = keysLoop(config, pgpKeys, noconfirm) + if keysRes == 'a': + return 1 - let workConfFileCopySuccess = try: - copyFile(confFile, workConfFile) + if editRes == 's': skipEdit = true + 0 + +proc writeMakepkgConf(config: Config; srcConf, destConf, pkgdest: string): bool = + ## Copies srcConf to destConf and appends pakku override stanzas. + ## pkgdest is the effective PKGDEST to write (caller resolves packageOutputDir). + ## Returns true on success. + try: + copyFile(srcConf, destConf) var file: File - if file.open(workConfFile, fmAppend): + if file.open(destConf, fmAppend): try: file.writeLine("") file.writeLine('#'.repeat(73)) file.writeLine("# PAKKU OVERRIDES") file.writeLine('#'.repeat(73)) - file.writeLine("CARCH=" & config.common.arch.bashEscape) - file.writeLine("PKGDEST=" & config.tmpRootInitial.bashEscape) + file.writeLine("CARCH=", config.common.arch.bashEscape) + file.writeLine("PKGDEST=", pkgdest.bashEscape) finally: file.close() true except CatchableError: - discard unlink(cstring(workConfFile)) + discard unlink(cstring(destConf)) false - if not workConfFileCopySuccess: - printError(config.color, tr"failed to copy config file '$#'" % [confFile]) - (none(BuildResult), 1, false) - else: - let envExt = getEnv("PKGEXT") - let confExt = if envExt.len == 0: - forkWaitRedirect(() => (block: - dropPrivRedirectAndChdir(none(string)): - execRedirect(bashCmd, "-c", - "source \"$@\" && echo \"$PKGEXT\"", - "bash", workConfFile))) - .output.optFirst.get("") - else: - envExt - - let (buildCode, interrupted) = catchInterrupt(): - forkWait(proc: int = - discard cunsetenv("MAKEPKG_CONF") - dropPrivilegesAndChdir(some(buildPath)): - if not noextract: - removeDirQuiet(buildPath & "src") - - let optional: seq[tuple[arg: string, cond: bool]] = @[ - ("--noextract", noextract), - ("--nocolor", not config.color), - ("--ignorearch", config.ignoreArch), - ("--nodeps", skipDeps) - ] - - execResult(@[makepkgCmd, "--config", workConfFile, "--force"] & - optional.filter(o => o.cond).map(o => o.arg))) - - discard unlink(cstring(workConfFile)) - - if interrupted: - (none(BuildResult), buildCode, interrupted) - elif buildCode != 0: - printError(config.color, tr"failed to build '$#'" % [base]) - (none(BuildResult), buildCode, false) +proc readConfExt(config: Config; workConfFile: string): string = + ## Reads PKGEXT from the environment or, failing that, sources the makepkg + ## conf to find the configured extension. Falls back to the glob pattern. + let envExt = getEnv("PKGEXT") + if envExt.len > 0: + return envExt + let ex = exec(config.color, + [bashCmd, "-c", "source \"$@\" && echo \"$PKGEXT\"", "bash", workConfFile], + none(string), emRedirect, true).output.optFirst.get("") + if ex.len > 0: ex else: PkgExtGlob + +proc matchArtifactsToInfos(config: Config; pkgInfos: seq[PackageInfo]; + resultPkgInfos: seq[PackageInfo]): Option[seq[ReplacePkgInfo]] = + ## Maps each requested PackageInfo to the corresponding post-build info, + ## falling back to index-based matching when counts agree. + ## Returns none on any mismatch. + requireNonEmpty(pkgInfos, "matchArtifactsToInfos") + let resultTable = block: + var t = initTable[string, PackageInfo]() + for ri in resultPkgInfos: t[ri.rpc.name] = ri + t + + let sameCount = pkgInfos[0].baseCount == resultPkgInfos.len + var targets = newSeq[ReplacePkgInfo]() + var failed = newSeq[string]() + var seen = initHashSet[string]() + + for idx, pi in pkgInfos: + var found = resultTable.opt(pi.rpc.name) + if found.isNone and sameCount: + found = some(resultPkgInfos[pi.baseIndex]) + if found.isNone: + failed.add pi.rpc.name else: - let resultPkgInfos = reloadPkgInfos(config, buildPath, pkgInfos) - - type ResultInfo = tuple[name: string, baseIndex: int, pkgInfo: Option[PackageInfo]] - - let resultPkgInfosTable = resultPkgInfos.map(i => (i.rpc.name, i)).toTable - let resultByNames: seq[ResultInfo] = pkgInfos - .map(i => (i.rpc.name, i.baseIndex, resultPkgInfosTable.opt(i.rpc.name))) - - let resultByIndices: seq[ResultInfo] = if pkgInfos[0].baseCount == resultPkgInfos.len: - resultByNames.map(res => (block: - if res.pkgInfo.isNone: - (res.name, res.baseIndex, some(resultPkgInfos[res.baseIndex])) - else: - res)) - else: - resultByNames - - let failedNames = collect(newSeq): - for x in resultByIndices: - if x.pkgInfo.isNone: - x.name + targets.add ReplacePkgInfo(name: some(pi.rpc.name), pkgInfo: found.unsafeGet) + seen.incl found.unsafeGet.rpc.name + + # Include split-package siblings that aren't explicitly in the install set. + for ri in resultPkgInfos: + if ri.rpc.name notin seen: + targets.add ReplacePkgInfo(name: none(string), pkgInfo: ri) + + if failed.len > 0: + for name in failed: + printError(config.color, tr"$#: failed to extract package info" % [name]) + none(seq[ReplacePkgInfo]) + else: + some(targets) - if failedNames.len > 0: - for name in failedNames: - printError(config.color, tr"$#: failed to extract package info" % [name]) - (none(BuildResult), 1, false) - else: - let targetPkgInfos: seq[ReplacePkgInfo] = resultByIndices - .map(i => (some(i.name), i.pkgInfo.get)) - let filterNames = targetPkgInfos.map(r => r.pkgInfo.rpc.name).toHashSet - let additionalPkgInfos: seq[ReplacePkgInfo] = resultPkgInfos - .filter(i => not (i.rpc.name in filterNames)) - .map(i => (none(string), i)) - (some(($confExt, targetPkgInfos & additionalPkgInfos)), 0, false) - -proc buildFromSources(config: Config, commonArgs: seq[Argument], - pkgInfos: seq[PackageInfo], skipDeps: bool, noconfirm: bool, trunkPath: bool): - tuple[buildResult: Option[BuildResult], code: int, skipped: bool] = +proc buildLoop(config: Config; pkgInfos: seq[PackageInfo]; skipDeps, noconfirm, + noextract: bool; confFile: string): (Option[BuildResult], int, bool) = + ## Runs makepkg for one package base and returns discovered package artifacts + ## paired with refreshed package metadata. + requireNonEmpty(pkgInfos, "buildLoop") let base = pkgInfos[0].rpc.base - var repoPath = repoPath(config.tmpRootInitial, base) + let repoPath = repoPath(config.tmpRootInitial, base) let gitSubdir = pkgInfos[0].rpc.gitSubdir + var buildPath = buildPath(repoPath, gitSubdir) + if pkgInfos[0].rpc.gitUrl.isValidPackagesUrl(): + buildPath = buildPath / "trunk/" - proc loop(noextract: bool, showEditLoop: bool): tuple[buildResult: Option[BuildResult], - code: int, skipped: bool] = - let res = if showEditLoop and not noconfirm: - editLoop(config, pkgInfos[0].rpc.repo, base, repoPath, gitSubdir, false, noconfirm, trunkPath) - else: - 'n' + let stagingDir = config.tmpRootInitial + let internalPkgdest = if config.packageOutputDir.len > 0: + config.packageOutputDir else: stagingDir + let workConfFile = stagingDir / "makepkg.conf" - if res == 'a': - (none(BuildResult), 1, false) - else: - let (buildResult, code, interrupted) = buildLoop(config, pkgInfos, - skipDeps, noconfirm, noextract) - - if interrupted: - (buildResult, 1, false) - elif code != 0: - let res = printColonUserChoiceWithHelp(config.color, - tr"Build failed. Retry, skip this package, or abort?", - choices('y', ('e', tr"retry with --noextract option"), - ('s', tr"skip this package"), ('a', tr"abort operation")), - 's', noconfirm, 's') - - if res == 'e': - loop(true, true) - elif res == 'y': - loop(false, true) - elif res == 's': - printWarning(config.color, tr"skipping package '$#'" % [base]) - (none(BuildResult), 0, true) - else: - (buildResult, code, false) - else: - (buildResult, code, false) + if not writeMakepkgConf(config, confFile, workConfFile, internalPkgdest): + printError(config.color, tr"failed to copy config file '$#'" % [confFile]) + return (none(BuildResult), 1, false) - if trunkPath == true: - repoPath.add("/trunk") - let preBuildCode = if config.preBuildCommand.isSome: (block: - printColon(config.color, tr"Running pre-build command...") + let confExt = readConfExt(config, workConfFile) + + let (buildCode, interrupted) = catchInterrupt(): + proc buildChild(): int = + discard cunsetenv("MAKEPKG_CONF") + if not noextract: + removeDirQuiet(buildPath / "src") + var cmd = @[makepkgCmd, "--config", workConfFile, "--force"] + for (arg, pred) in [ + ("--noextract", noextract), + ("--nocolor", not config.color), + ("--ignorearch", config.ignoreArch), + ("--nodeps", skipDeps) + ]: + if pred: cmd.add arg + execResult(cmd) + exec(config.color, buildChild, some(buildPath), emNormal, true).code + + discard unlink(cstring(workConfFile)) + + if interrupted: + return (none(BuildResult), buildCode, true) + if buildCode != 0: + printError(config.color, tr"failed to build '$#'" % [base]) + return (none(BuildResult), buildCode, false) - let code = forkWait(() => (block: - dropPrivilegesAndChdir(some(buildPath(repoPath, gitSubdir))): - execResult(bashCmd, "-c", config.preBuildCommand.unsafeGet))) + let resultPkgInfos = reloadPkgInfos(config, buildPath, pkgInfos) + let replacePkgInfosOpt = matchArtifactsToInfos(config, pkgInfos, resultPkgInfos) + if replacePkgInfosOpt.isNone: + return (none(BuildResult), 1, false) - if code != 0 and printColonUserChoice(config.color, - tr"Command failed, continue?", ['y', 'n'], 'n', 'n', - noconfirm, 'n') == 'y': - 0 - else: - code) - else: - 0 + let artifacts = findArtifactsInDir(config, internalPkgdest, + replacePkgInfosOpt.unsafeGet, reportErrors = true, extGlob = confExt) + if artifacts.len == 0: + return (none(BuildResult), 1, false) - if preBuildCode != 0: - (none(BuildResult), preBuildCode, false) + (some(BuildResult(artifacts: artifacts)), 0, false) + +proc tryBuild(config: Config; pkgInfos: seq[PackageInfo]; skipDeps, noconfirm, + trunkPath: bool; confFile, repoPath: string; gitSubdir: Option[string]; + base: string; noextract, showEditLoop: bool): + tuple[buildResult: Option[BuildResult], code: int, skipped: bool] = + ## Build or retry one package base with interactive error handling. + requireNonEmpty(pkgInfos, "tryBuild") + if showEditLoop and not noconfirm: + let res = editLoop(config, pkgInfos[0].rpc.repo, base, repoPath, gitSubdir, + false, noconfirm, trunkPath) + if res == 'a': + return (none(BuildResult), 1, false) + + let (buildResult, code, interrupted) = buildLoop(config, pkgInfos, + skipDeps, noconfirm, noextract, confFile) + + if interrupted: + return (buildResult, 1, false) + if code == 0: + return (buildResult, 0, false) + + let res = printColonUserChoiceWithHelp(config.color, + tr"Build failed. Retry, skip this package, or abort?", + choices('y', ('e', tr"retry with --noextract option"), + ('s', tr"skip this package"), ('a', tr"abort operation")), + 's', noconfirm, 's') + + case res: + of 'e': + tryBuild(config, pkgInfos, skipDeps, noconfirm, trunkPath, confFile, + repoPath, gitSubdir, base, true, true) + of 'y': + tryBuild(config, pkgInfos, skipDeps, noconfirm, trunkPath, confFile, + repoPath, gitSubdir, base, false, true) + of 's': + printWarning(config.color, tr"skipping package '$#'" % [base]) + (none(BuildResult), 0, true) else: - loop(false, false) - -proc installGroupFromSources(config: Config, commonArgs: seq[Argument], - basePackages: seq[seq[PackageInfo]], explicits: HashSet[string], - skipDeps: bool, noconfirm: bool): tuple[ - installedAs: seq[(string, string)], - code: int, - keepBuiltArtifacts: bool - ] = - var lastBase: string - var trunkPath: bool - - proc buildNext(index: int, buildResults: List[BuildResult]): (List[BuildResult], int) = - if index < basePackages.len: - lastBase = basePackages[index][0].rpc.base - if contains(basePackages[index][0].rpc.gitUrl, "https://github.com/archlinux/") or basePackages[index][0].rpc.gitUrl == "https://gitea.artixlinux.org/packages": - trunkPath = true - else: - trunkPath = false - let (buildResult, code, skipped) = buildFromSources(config, commonArgs, - basePackages[index], skipDeps, noconfirm, trunkPath) - - if code != 0: - (buildResults.reversed, code) - elif skipped: - buildNext(index + 1, buildResults) - else: - buildNext(index + 1, buildResult.unsafeGet ^& buildResults) - else: - (buildResults.reversed, 0) - - let (buildResults, buildCode) = buildNext(0, nil) - - proc formatArchiveFile(pkgInfo: PackageInfo, ext: string): string = - let arch = if pkgInfo.archs.len > 0: config.common.arch else: "any" - config.tmpRootInitial & "/" & pkgInfo.rpc.name & "-" & pkgInfo.rpc.version & "-" & arch & ext + (buildResult, code, false) + +proc buildFromSources(config: Config; pkgInfos: seq[PackageInfo]; + skipDeps, noconfirm, trunkPath: bool; confFile: string): + tuple[buildResult: Option[BuildResult], code: int, skipped: bool] = + ## Wraps buildLoop with pre-build hooks and interactive retry/skip handling + ## for a single package base. + requireNonEmpty(pkgInfos, "buildFromSources") + let base = pkgInfos[0].rpc.base + let repoPath = repoPath(config.tmpRootInitial, base) + let gitSubdir = pkgInfos[0].rpc.gitSubdir - let allFiles = collect(newSeq): - for br in buildResults: - for r in br.replacePkgInfos: - (name:r.name, file:formatArchiveFile(r.pkgInfo, br.ext)) - let filesTable = allFiles.filter(f => f.name.isSome).map(f => (f.name.unsafeGet, f.file)).toTable + if config.preBuildCommand.isSome: + printColon(config.color, tr"Running pre-build command...") + # Run pre-build command in the trunk subdir for official-repo packages, + # otherwise in the gitSubdir (or repo root when none). + let preBuildDir = if trunkPath: repoPath / "trunk" + else: buildPath(repoPath, gitSubdir) + let code = exec(config.color, [bashCmd, "-c", config.preBuildCommand.unsafeGet], + some(preBuildDir), emNormal, true).code + if code != 0 and printColonUserChoice(config.color, + tr"Command failed, continue?", ['y', 'n'], 'n', 'n', noconfirm, 'n') != 'y': + return (none(BuildResult), code, false) + + tryBuild(config, pkgInfos, skipDeps, noconfirm, trunkPath, confFile, + repoPath, gitSubdir, base, false, false) + + + +func resolveInstallMode(pkgName: string; explicit: bool; + local: ptr AlpmDatabase): InstallMode = + ## Determines install mode based on the requested reason and the + ## current reason stored in the local database. + ## Returning "auto" avoids an unnecessary pacman -D roundtrip for packages + ## whose reason already matches. + let package = local[cstring(pkgName)] + if package != nil: + let installedExplicitly = package.reason == AlpmReason.explicit + if explicit == installedExplicitly: InstallMode.auto + elif explicit: InstallMode.explicit + else: InstallMode.dependency + elif explicit: InstallMode.auto + else: InstallMode.dependency + +proc buildAllBases(config: Config; commonArgs: seq[Argument]; + basePackages: seq[seq[PackageInfo]]; skipDeps, noconfirm: bool; + confFile, savedTo, effectivePkgdest: string): BuildBasesRes = + ## Build or skip each base in dependency order. On build failure, chmods the + ## pkg directory and returns early. + var results: seq[BuildResult] + var skippedCount = 0 + for index in 0 ..< basePackages.len: + requireNonEmpty(basePackages[index], "buildAllBases") + let baseName = basePackages[index][0].rpc.base + let isTrunkPath = basePackages[index][0].rpc.gitUrl.isValidPackagesUrl() + let reusable = findReusableArtifacts(config, basePackages[index], + savedTo, effectivePkgdest) + if reusable.len > 0: + results.add BuildResult(artifacts: reusable) + continue + + let (buildResult, code, skipped) = buildFromSources( + config, basePackages[index], skipDeps, noconfirm, isTrunkPath, confFile) + if skipped: + inc skippedCount + continue + if code != 0: + let path = config.tmpRootInitial / baseName / (if isTrunkPath: "trunk/pkg" else: "pkg") + discard chmod(cstring(path), 0o0755) + return BuildBasesRes(results: results, skipped: skippedCount, code: code) + results.add buildResult.unsafeGet + + assert results.len + skippedCount == basePackages.len, + "results + skipped != incoming base count" + BuildBasesRes(results: results, skipped: skippedCount, code: 0) + +func prepareArtifacts(buildResults: openArray[BuildResult]; + basePackages: openArray[seq[PackageInfo]]): InstallArtifacts = + ## Collect built artifacts into install targets and track built bases. + ## installFiles is precomputed here so cleanup never rebuilds it per-call. + var artifacts: seq[BuildArtifact] + for br in buildResults: + for artifact in br.artifacts: + artifacts.add artifact + + let filesTable = block: + var table = initTable[string, string]() + for a in artifacts: + if a.name.isSome: table[a.name.unsafeGet] = a.file + table let install = collect(newSeq): for g in basePackages: for i in g: for x in filesTable.opt(i.rpc.name): - (name:i.rpc.name,file:x) - + InstallTarget(name: i.rpc.name, file: x) + let installFiles = block: + var s = initHashSet[string]() + for t in install: s.incl t.file + s var builtBases = initHashSet[string]() - for br in buildResults: - for r in br.replacePkgInfos: - builtBases.incl(r.pkgInfo.rpc.base) - - proc cleanupBuiltArtifacts(clear: bool) = - let installFiles = install.map(p => p.file) - for pair in allFiles: - if clear or not (pair.file in installFiles): - try: - removeFile(pair.file) - except CatchableError: - discard - - if not clear: - printWarning(config.color, tr"packages are saved to '$#'" % [config.tmpRootInitial]) - - if buildCode != 0: - if trunkPath == true: - discard chmod(cstring(config.tmpRootInitial & "/" & lastBase & "/trunk/pkg"), 0o0755) - else: - discard chmod(cstring(config.tmpRootInitial & "/" & lastBase & "/pkg"), 0o0755) - cleanupBuiltArtifacts(true) - (installedAs: newSeq[(string, string)](), code: buildCode, keepBuiltArtifacts: false) - elif install.len == 0: - cleanupBuiltArtifacts(false) - (installedAs: newSeq[(string, string)](), code: 0, keepBuiltArtifacts: false) - else: - if currentUser.uid != 0 and printColonUserChoice(config.color, - tr"Continue installing?", ['y', 'n'], 'y', 'n', - noconfirm, 'y') != 'y': - cleanupBuiltArtifacts(false) - (installedAs: newSeq[(string, string)](), code: 1, keepBuiltArtifacts: true) + var renames: CanonicalRenames + for a in artifacts: + builtBases.incl a.pkgInfo.rpc.base + if a.name.isSome: + let aname = a.name.unsafeGet + if aname != a.pkgInfo.rpc.name: + assert aname notin renames, "duplicate canonicalization key" + renames[aname] = a.pkgInfo.rpc.name + + assert installFiles.len == install.len, "mismatched install targets vs files" + assert install.len <= artifacts.len, "more install targets than artifacts" + InstallArtifacts(artifacts: artifacts, install: install, + installFiles: installFiles, builtBases: builtBases, renames: renames) + +proc tagBuiltBases(config: Config; basePackages: openArray[seq[PackageInfo]]; + builtBases: HashSet[string]) = + ## Create git view tags in bare-repo caches for built AUR packages. + let cachePath = config.userCacheInitial.cache(CacheKind.repositories) + for pkgInfos in basePackages: + requireNonEmpty(pkgInfos, "tagBuiltBases") + let repo = pkgInfos[0].rpc.repo + if repo != config.aurRepo or pkgInfos[0].rpc.base notin builtBases: + continue + let base = pkgInfos[0].rpc.base + let bareRepoPath = repoPath(cachePath, bareFullName(BareKind.pkg, base)) + let tag = createViewTag(repo, base) + discard exec(config.color, [gitCmd, "-C", bareRepoPath, "tag", "-d", tag], + none(string), emSilent, true) + discard exec(config.color, [gitCmd, "-C", bareRepoPath, "tag", tag], + none(string), emSilent, true) + +proc installGroupFromSources(config: Config; commonArgs: seq[Argument]; + basePackages: seq[seq[PackageInfo]]; explicits: HashSet[string]; + skipDeps, noconfirm: bool; confFile, effectivePkgdest: string): InstallGroupRes = + ## Builds a dependency-ordered group of source packages, chooses which archives + ## to install, invokes the privileged install helper, and cleans or preserves + ## the build artifacts according to the install outcome. + let archivesOutsideTmp = config.packageOutputDir.len > 0 + let savedTo = if archivesOutsideTmp: config.packageOutputDir + else: config.tmpRootInitial + + let successSpec = cleanupSpec(config.cleanupAfterInstall, archivesOutsideTmp) + let failureSpec = cleanupSpecForFailure(buildFailure, config, archivesOutsideTmp) + + let build = buildAllBases(config, commonArgs, basePackages, skipDeps, + noconfirm, confFile, savedTo, effectivePkgdest) + let artifacts = prepareArtifacts(build.results, basePackages) + assert build.results.len + build.skipped <= basePackages.len + assert build.code == 0 or build.results.len + build.skipped < basePackages.len, + "incomplete results expected when build failed" + + if build.code != 0: + cleanupArtifacts(config, artifacts, savedTo, failureSpec) + return InstallGroupRes(code: build.code) + + if artifacts.install.len == 0: + assert build.code == 0 + assert build.results.len == 0, "build produced no install targets" + cleanupArtifacts(config, artifacts, savedTo, successSpec) + return InstallGroupRes(code: 0) + + if currentUser.uid != 0 and printColonUserChoice(config.color, + tr"Continue installing?", ['y', 'n'], 'y', 'n', noconfirm, 'y') != 'y': + cleanupArtifacts(config, artifacts, savedTo, successSpec) + return InstallGroupRes(code: 1) + + let installWithReason = withAlpmConfig(config, false, handle, dbs, errors): + let local = handle.local + collect(newSeq): + for it in artifacts.install: + InstallItem(name: it.name, + file: it.file, + mode: $resolveInstallMode(it.name, it.name in explicits, local)) + + let (cacheDir, cacheUser, cacheGroup) = + if config.preserveBuilt == PreserveBuilt.internal: + (config.cache, 0, 0) + elif config.preserveBuilt == PreserveBuilt.user: + let error = ensureUserCacheOrError(config, CacheKind.packages, true) + for e in error: printError(config.color, e) + let user = initialUser.get(currentUser) + (config.userCacheInitial.cache(CacheKind.packages), user.uid, user.gid) + elif config.preserveBuilt == PreserveBuilt.pkgdest and effectivePkgdest.len > 0: + let user = initialUser.get(currentUser) + (effectivePkgdest, user.uid, user.gid) else: - let installWithReason = withAlpmConfig(config, false, handle, dbs, errors): - let local = handle.local - install.map(proc (pkg: auto): tuple[name: string, file: string, mode: string] = - let explicit = pkg.name in explicits - let package = local[cstring(pkg.name)] - let mode = if package != nil: (block: - let installedExplicitly = package.reason == AlpmReason.explicit - if explicit == installedExplicitly: - "auto" - elif explicit: - "explicit" - else: - "dependency") - elif explicit: - "auto" - else: - "dependency" - (pkg.name, pkg.file, mode)) - - let (cacheDir, cacheUser, cacheGroup) = if config.preserveBuilt == PreserveBuilt.internal: - (config.cache, 0, 0) - elif config.preserveBuilt == PreserveBuilt.user: (block: - let error = ensureUserCacheOrError(config, CacheKind.packages, true) - for e in error: printError(config.color, e) - let user = initialUser.get(currentUser) - let dir = config.userCacheInitial.cache(CacheKind.packages) - (dir, user.uid, user.gid)) - else: - # pass -1 values to disable caching - ("", -1, -1) - - let pacmanUpgradeParams = pacmanCmd & pacmanParams(config.color, - commonArgs & ("U", none(string), ArgumentType.short)) - - let pacmanDatabaseParams = pacmanCmd & pacmanParams(config.color, - commonArgs.keepOnlyOptions(commonOptions) & ("D", none(string), ArgumentType.short)) - - let installParams = block: - var p = config.sudoCommand - p.add helperToolCommand("install") - p.add [cacheDir, $cacheUser, $cacheGroup, $pacmanUpgradeParams.len] - p.add pacmanUpgradeParams - p.add $pacmanDatabaseParams.len - p.add pacmanDatabaseParams - for i in installWithReason: p.add [i.name, i.file, i.mode] - p - - let code = forkWait(() => execResult(installParams)) - if code != 0: - cleanupBuiltArtifacts(false) - (installedAs: newSeq[(string, string)](), code: code, keepBuiltArtifacts: true) - else: - let cachePath = config.userCacheInitial.cache(CacheKind.repositories) - for pkgInfos in basePackages: - let repo = pkgInfos[0].rpc.repo - if repo == config.aurRepo and pkgInfos[0].rpc.base in builtBases: - let base = pkgInfos[0].rpc.base - let fullName = bareFullName(BareKind.pkg, base) - let bareRepoPath = repoPath(cachePath, fullName) - let tag = createViewTag(repo, base) - - template run(args: varargs[string]) = - discard forkWait(() => (block: - dropPrivilegesAndChdir(none(string)): - if not config.common.debug: - discard close(1) - discard open("/dev/null") - discard close(2) - discard open("/dev/null") - execResult(args))) - - run(gitCmd, "-C", bareRepoPath, "tag", "-d", tag) - run(gitCmd, "-C", bareRepoPath, "tag", tag) - - cleanupBuiltArtifacts(true) - let installedAs = collect(newSeq): - for br in buildResults: - for r in br.replacePkgInfos: - if r.name.isSome: - (r.name.unsafeGet, r.pkgInfo.rpc.name) - (installedAs: installedAs, code: 0, keepBuiltArtifacts: false) - -proc deduplicatePkgInfos(pkgInfos: seq[PackageInfo], - config: Config, printWarning: bool): seq[PackageInfo] = - pkgInfos.foldl(block: - if a.map(i => i.rpc.name).contains(b.rpc.name): - if printWarning: - printWarning(config.color, trp("skipping target: %s\n") % [b.rpc.name]) - a + ("", -1, -1) + + let pacmanUpgradeParams = pacmanCmd & pacmanParams(config.color, + commonArgs & ("U", none(string), ArgumentType.short)) + let pacmanDatabaseParams = pacmanCmd & pacmanParams(config.color, + commonArgs.keepOnlyOptions(commonOptions) & ("D", none(string), ArgumentType.short)) + + assert pacmanUpgradeParams.len > 0 and pacmanDatabaseParams.len > 0, + "install helper protocol requires non-empty pacman command vectors" + assert installWithReason.len > 0 + + let installParams = block: + var p = config.sudoCommand + p.add helperToolCommand("install") + p.add [cacheDir, $cacheUser, $cacheGroup, $pacmanUpgradeParams.len] + p.add pacmanUpgradeParams + p.add $pacmanDatabaseParams.len + p.add pacmanDatabaseParams + for i in installWithReason: + p.add [i.name, i.file, i.mode] + p + + let code = forkWait(() => execResult(installParams)) + if code != 0: + # Pacman failed: preserve artifacts so the user can inspect or retry. + let installFailureSpec = cleanupSpecForFailure(installFailure, config, archivesOutsideTmp) + cleanupArtifacts(config, artifacts, savedTo, installFailureSpec) + return InstallGroupRes(code: code) + + tagBuiltBases(config, basePackages, artifacts.builtBases) + cleanupArtifacts(config, artifacts, savedTo, successSpec) + + InstallGroupRes(canonicalRenames: artifacts.renames, code: 0) + +proc deduplicatePkgInfos(pkgInfos: seq[PackageInfo]; + config: Config; warnOnDup: bool): seq[PackageInfo] = + ## Deduplicate by package name, optionally warning on duplicates. + var seen = initHashSet[string]() + for pi in pkgInfos: + if pi.rpc.name in seen: + if warnOnDup: + printWarning(config.color, trp("skipping target: %s\n") % [pi.rpc.name]) else: - a & b, - newSeq[PackageInfo]()) - -proc resolveDependencies(config: Config, pkgInfos: seq[PackageInfo], - additionalPkgInfos: seq[PackageInfo], printMode: bool, - nodepsCount: int, assumeInstalled: seq[PackageReference], noaur: bool): - (bool, Table[PackageReference, SatisfyResult], - seq[string], seq[seq[seq[PackageInfo]]], seq[string]) = + seen.incl pi.rpc.name + result.add pi + +proc resolveDependencies(config: Config; pkgInfos: seq[PackageInfo]; + additionalPkgInfos: seq[PackageInfo]; printMode: bool; + nodepsCount: int; assumeInstalled: seq[PackageReference]; noaur: bool): + (bool, Table[PackageReference, SatisfyResult], + seq[string], seq[seq[seq[PackageInfo]]], seq[string]) = if pkgInfos.len > 0 and not printMode: echo(trp("resolving dependencies...\n")) let (satisfied, unsatisfied, paths) = withAlpmConfig(config, true, handle, dbs, errors): findDependencies(config, handle, dbs, pkgInfos, additionalPkgInfos, nodepsCount, assumeInstalled, printMode, noaur) - let buildAndAurNamesSet = pkgInfos.map(i => i.rpc.name).toHashSet - let fullPkgInfos = (pkgInfos & (block:collect(newSeq): - for s in satisfied.values: - for i in s.buildPkgInfo: - if not (i.rpc.name in buildAndAurNamesSet): - i - )).deduplicatePkgInfos(config,false) - let additionalPacmanTargets = collect(newSeq): - for x in satisfied.values: - if not x.installed and x.buildPkgInfo.isNone: - x.name + let (fullPkgInfos, additionalPacmanTargets) = block: + var buildAndAurNamesSet = initHashSet[string]() + var all = pkgInfos + var pacmanTargets: seq[string] + + for pkgInfo in pkgInfos: + buildAndAurNamesSet.incl pkgInfo.rpc.name + + for satres in satisfied.values: + if satres.buildPkgInfo.isSome: + let pkgInfo = satres.buildPkgInfo.unsafeGet + if pkgInfo.rpc.name notin buildAndAurNamesSet: + all.add pkgInfo + elif not satres.installed: + pacmanTargets.add satres.name + (all.deduplicatePkgInfos(config, false), pacmanTargets) + let orderedPkgInfos = orderInstallation(fullPkgInfos, satisfied) - if unsatisfied.len > 0: # dependency not found + + if unsatisfied.len > 0: printUnsatisfied(config, satisfied, unsatisfied) (false, satisfied, additionalPacmanTargets, orderedPkgInfos, paths) else: (true, satisfied, additionalPacmanTargets, orderedPkgInfos, paths) -proc confirmViewAndImportKeys(config: Config, basePackages: seq[seq[seq[PackageInfo]]], - installed: seq[Installed], noconfirm: bool): int = - if basePackages.len > 0: (block: - let installedVersions = installed.map(i => (i.name, i.version)).toTable - printPackages(config.color, config.common.verbosePkgLists,(block:collect(newSeq): - for g in basePackages: - for b in g: - for i in b: - (i.rpc.name,i.rpc.repo,installedVersions.opt(i.rpc.name),i.rpc.version).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 flatBasePackages = collect(newSeq): - for a in basePackages: - for x in a: - x - - proc checkNext(index: int, skipEdit: bool, skipKeys: bool): int = - if index < flatBasePackages.len: - var trunkPath: bool = false - var resultPkgInfos: seq[PackageInfo] - let pkgInfos = flatBasePackages[index] - let repo = pkgInfos[0].rpc.repo - let base = pkgInfos[0].rpc.base - let repoPath = repoPath(config.tmpRootInitial, base) - if contains(pkgInfos[0].rpc.gitUrl, "https://github.com/archlinux/") or pkgInfos[0].rpc.gitUrl == "https://gitea.artixlinux.org/packages": - trunkPath = true - - let aur = repo == config.aurRepo - - if not skipEdit and aur and not noconfirm 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].rpc.maintainer, commentsReversed) - - let editRes = if skipEdit or noconfirm: - 'n' - else: (block: - let defaultYes = aur and not config.viewNoDefault - editLoop(config, repo, base, repoPath, pkgInfos[0].rpc.gitSubdir, - defaultYes, noconfirm, trunkPath)) - - if editRes == 'a': - 1 - else: - if trunkPath == true: - resultPkgInfos = reloadPkgInfos(config, repoPath & "/trunk/", pkgInfos) - else: - resultPkgInfos = reloadPkgInfos(config, - repoPath & "/" & pkgInfos[0].rpc.gitSubdir.get("."), pkgInfos) - let pgpKeys = deduplicate: - collect(newSeq): - for p in resultPkgInfos: - for x in p.pgpKeys: - x - - proc keysLoop(index: int, skipKeys: bool): char = - if index >= pgpKeys.len: - 'n' - elif forkWait(() => (block: - discard close(0) - discard open("/dev/null") - discard close(1) - discard open("/dev/null") - discard close(2) - discard open("/dev/null") - dropPrivilegesAndChdir(none(string)): - execResult(gpgCmd, "--list-keys", pgpKeys[index]))) == 0: - keysLoop(index + 1, skipKeys) - else: - let res = if skipKeys: - 'y' - else: - printColonUserChoiceWithHelp(config.color, - tr"Import PGP key $#?" % [pgpKeys[index]], - choices('y', 'n', ('c', tr"import all keys"), ('a', tr"abort operation")), - 'y', noconfirm, 'y') - - let newSkipKeys = skipKeys or res == 'c' - - if res == 'y' or newSkipKeys: - let importCode = forkWait(() => (block: - dropPrivilegesAndChdir(none(string)): - if config.common.pgpKeyserver.isSome: - forkWait(() => execResult(gpgCmd, - "--keyserver", config.common.pgpKeyserver.unsafeGet, - "--recv-keys", pgpKeys[index])) - else: - forkWait(() => execResult(gpgCmd, - "--recv-keys", pgpKeys[index])))) - - if importCode == 0 or newSkipKeys or noconfirm: - keysLoop(index + 1, newSkipKeys) - else: - if importCode != 0: - echo(tr"Error - gpg return code = ", importCode) - keysLoop(index, newSkipKeys) - elif res == 'n': - keysLoop(index + 1, newSkipKeys) - else: - res - - let keysRes = keysLoop(0, skipKeys) - if keysRes == 'a': - 1 - else: - checkNext(index + 1, skipEdit or editRes == 's', skipKeys or keysRes == 's') - else: - 0 - - checkNext(0, false, false) +proc confirmViewAndImportKeys(config: Config; basePackages: seq[seq[seq[PackageInfo]]]; + installed: seq[Installed]; noconfirm: bool): int = + if basePackages.len == 0: + return 0 + let installedVersions = installed.mapIt((it.name, it.version)).toTable + printPackages(config.color, config.common.verbosePkgLists, (block: collect(newSeq): + for g in basePackages: + for b in g: + for i in b: + (i.rpc.name, i.rpc.repo, installedVersions.opt(i.rpc.name), + i.rpc.version).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': + return 1 + + let flatBasePackages = collect(newSeq): + for a in basePackages: + for x in a: x + checkNext(config, flatBasePackages, noconfirm) + +proc removeBuildDependencies(config: Config; commonArgs: seq[Argument]; + unrequired, unrequiredOptional: HashSet[string]): int = + if unrequired.len == 0 and unrequiredOptional.len == 0: + return 0 + + let removeArgs = commonArgs.keepOnlyOptions(commonOptions, transactionOptions) + + let code = if unrequired.len > 0: + printColon(config.color, tr"Removing build dependencies...") + let s = unrequired.targetArguments + pacmanRun(some(config.sudoCommand), config.color, + removeArgs & ("R", none(string), ArgumentType.short) & s) else: - 1) - else: - 0 - -proc removeBuildDependencies(config: Config, commonArgs: seq[Argument], - unrequired: HashSet[string], unrequiredOptional: HashSet[string]): int = - if unrequired.len > 0 or unrequiredOptional.len > 0: (block: - let removeArgs = commonArgs.keepOnlyOptions(commonOptions, transactionOptions) + 0 - let code = if unrequired.len > 0: (block: - printColon(config.color, tr"Removing build dependencies...") - pacmanRun(some config.sudoCommand, config.color, removeArgs & - ("R", none(string), ArgumentType.short) & - toSeq(unrequired.items).map(t => (t, none(string), ArgumentType.target)))) - else: - 0 + if code != 0: return code - if code == 0 and unrequiredOptional.len > 0: - printColon(config.color, tr"Removing optional build dependencies...") - pacmanRun(some config.sudoCommand, config.color, removeArgs & - ("R", none(string), ArgumentType.short) & - toSeq(unrequiredOptional.items).map(t => (t, none(string), ArgumentType.target))) - else: - code) + if unrequiredOptional.len > 0: + printColon(config.color, tr"Removing optional build dependencies...") + let s = unrequiredOptional.targetArguments + pacmanRun(some config.sudoCommand, config.color, + removeArgs & ("R", none(string), ArgumentType.short) & s) else: 0 -proc printAllWarnings(config: Config, installed: seq[Installed], rpcInfos: seq[RpcPackageInfo], - pkgInfos: seq[PackageInfo], acceptedPkgInfos: seq[PackageInfo], upToDateNeeded: seq[Installed], - buildUpToDateNeeded: seq[(string, string)], localIsNewerSeq: seq[LocalIsNewer], - targetNamesSet: HashSet[string], upgradeCount: int, noaur: bool) = - let acceptedSet = acceptedPkgInfos.map(i => i.rpc.name).toHashSet +proc printAllWarnings(config: Config; installed: seq[Installed]; + installedTable: Table[string, Installed]; rpcInfos: seq[RpcPackageInfo]; + pkgInfos, acceptedPkgInfos: seq[PackageInfo]; upToDateNeeded: seq[Installed]; + buildUpToDateNeeded: seq[(string, string)]; localIsNewerSeq: seq[LocalIsNewer]; + targetNamesSet: HashSet[string]; upgradeCount: int; noaur: bool) = + ## Print orphan, up-to-date, downgrade, and ignore warnings. + ## installedTable is passed in (already computed by the caller) to avoid rebuilding. + let acceptedSet = acceptedPkgInfos.mapIt(it.rpc.name).toHashSet if upgradeCount > 0 and not noaur and config.printAurNotFound: - let rpcInfoTable = rpcInfos.map(i => (i.name, i)).toTable + let rpcInfoTable = rpcInfos.mapIt((it.name, it)).toTable for inst in installed: if inst.foreign and not config.ignored(inst.name, inst.groups) and - not rpcInfoTable.hasKey(inst.name): + inst.name notin rpcInfoTable: printWarning(config.color, tr"$# was not found in AUR" % [inst.name]) if upgradeCount == 1 and config.printLocalIsNewer: - for localIsNewer in localIsNewerSeq: + for lin in localIsNewerSeq: printWarning(config.color, tra("%s: local (%s) is newer than %s (%s)\n") % - [localIsNewer.name, localIsNewer.version, config.aurRepo, localIsNewer.aurVersion]) + [lin.name, lin.version, config.aurRepo, lin.aurVersion]) for inst in upToDateNeeded: printWarning(config.color, tra("%s-%s is up to date -- skipping\n") % [inst.name, inst.version]) - for pair in buildUpToDateNeeded: - let (name, version) = pair + for (name, version) in buildUpToDateNeeded: printWarning(config.color, tra("%s-%s is up to date -- skipping\n") % [name, version]) for pkgInfo in pkgInfos: - let installedTable = installed.map(i => (i.name, i)).toTable - - if not (pkgInfo.rpc.name in acceptedSet): + if pkgInfo.rpc.name in acceptedSet: + if pkgInfo.rpc.repo == config.aurRepo: + if pkgInfo.rpc.maintainer.isNone: + printWarning(config.color, tr"$# is orphaned" % [pkgInfo.rpc.name]) + if pkgInfo.rpc.name in installedTable: + let instVer = installedTable[pkgInfo.rpc.name].version + let newVer = pkgInfo.rpc.version + if vercmp(cstring(newVer), cstring(instVer)) < 0 and not pkgInfo.rpc.name.isVcs: + printWarning(config.color, + tra("%s: downgrading from version %s to version %s\n") % + [pkgInfo.rpc.name, instVer, newVer]) + else: if not (pkgInfo.rpc.name in targetNamesSet) and upgradeCount > 0 and - installedTable.hasKey(pkgInfo.rpc.name): - let installedVersion = installedTable[pkgInfo.rpc.name].version - let newVersion = pkgInfo.rpc.version - if vercmp(cstring(newVersion), cstring(installedVersion)) < 0: - printWarning(config.color, tra("%s: ignoring package downgrade (%s => %s)\n") % - [pkgInfo.rpc.name, installedVersion, newVersion]) - else: - printWarning(config.color, tra("%s: ignoring package upgrade (%s => %s)\n") % - [pkgInfo.rpc.name, installedVersion, newVersion]) + pkgInfo.rpc.name in installedTable: + let instVer = installedTable[pkgInfo.rpc.name].version + let newVer = pkgInfo.rpc.version + let warnFmt = if vercmp(cstring(newVer), cstring(instVer)) < 0: + tra("%s: ignoring package downgrade (%s => %s)\n") + else: + tra("%s: ignoring package upgrade (%s => %s)\n") + printWarning(config.color, warnFmt % [pkgInfo.rpc.name, instVer, newVer]) else: printWarning(config.color, trp("skipping target: %s\n") % [pkgInfo.rpc.name]) - elif pkgInfo.rpc.repo == config.aurRepo: - if pkgInfo.rpc.maintainer.isNone: - printWarning(config.color, tr"$# is orphaned" % [pkgInfo.rpc.name]) - if installedTable.hasKey(pkgInfo.rpc.name): - let installedVersion = installedTable[pkgInfo.rpc.name].version - let newVersion = pkgInfo.rpc.version - if vercmp(cstring(newVersion), cstring(installedVersion)) < 0 and not pkgInfo.rpc.name.isVcs: - printWarning(config.color, - tra("%s: downgrading from version %s to version %s\n") % - [pkgInfo.rpc.name, installedVersion, newVersion]) - -proc filterIgnoresAndConflicts(config: Config, pkgInfos: seq[PackageInfo], - targetNamesSet: HashSet[string], installed: Table[string, Installed], - printMode: bool, noconfirm: bool): (seq[PackageInfo], seq[PackageInfo]) = - let acceptedPkgInfos = pkgInfos.filter(pkgInfo => (block: - let instGroups = collect(newSeq): - for i in installed.opt(pkgInfo.rpc.name): - for x in i.groups: - x - if config.ignored(pkgInfo.rpc.name, (instGroups & pkgInfo.groups).deduplicate): - if pkgInfo.rpc.name in targetNamesSet: - if not printMode: - let input = printColonUserChoice(config.color, +proc filterIgnoresAndConflicts(config: Config; pkgInfos: seq[PackageInfo]; + targetNamesSet: HashSet[string]; installed: Table[string, Installed]; + printMode, noconfirm: bool): (seq[PackageInfo], seq[PackageInfo]) = + let acceptedPkgInfos = block: + var accepted: seq[PackageInfo] + for pkgInfo in pkgInfos: + let groups = block: + var res = pkgInfo.groups + for inst in installed.opt(pkgInfo.rpc.name): + res = (inst.groups & res).deduplicate + res + + if not config.ignored(pkgInfo.rpc.name, groups): + accepted.add pkgInfo + elif pkgInfo.rpc.name in targetNamesSet: + if printMode or printColonUserChoice(config.color, trp"%s is in IgnorePkg/IgnoreGroup. Install anyway?" % [pkgInfo.rpc.name], - ['y', 'n'], 'y', 'n', noconfirm, 'y') - input != 'n' - else: - true + ['y', 'n'], 'y', 'n', noconfirm, 'y') != 'n': + accepted.add pkgInfo + accepted + + if printMode: + return (acceptedPkgInfos, acceptedPkgInfos) + + let nonConflicting = block: + var res: seq[PackageInfo] + for b in acceptedPkgInfos: + let bRef = b.rpc.toPackageReference + var conflictsWith = newSeq[string]() + for p in res: + let pRef = p.rpc.toPackageReference + if b.conflicts.anyIt(it.isProvidedBy(pRef, true)) or + p.conflicts.anyIt(it.isProvidedBy(bRef, true)): + conflictsWith.add p.rpc.name + + if conflictsWith.len > 0: + for conflictName in conflictsWith: + printWarning(config.color, + tra("removing '%s' from target list because it conflicts with '%s'\n") % + [b.rpc.name, conflictName]) else: - false - else: - true)) - - let nonConflicingPkgInfos = acceptedPkgInfos.foldl(block: - let conflictsWith = collect(newSeq): - for p in a: - if p.rpc.name != b.rpc.name and - (block:collect(newSeq): - for c in b.conflicts: - if c.isProvidedBy(p.rpc.toPackageReference, true): - 0 - ).len>0 or - (block:collect(newSeq): - for c in p.conflicts: - if c.isProvidedBy(p.rpc.toPackageReference, true): - 0 - ).len>0: - p - if not printMode and conflictsWith.len > 0: - for conflict in conflictsWith: - printWarning(config.color, - tra("removing '%s' from target list because it conflicts with '%s'\n") % - [b.rpc.name, conflict.rpc.name]) - a - else: - a & b, - newSeq[PackageInfo]()) + res.add b + res - (nonConflicingPkgInfos, acceptedPkgInfos) + (nonConflicting, acceptedPkgInfos) -proc checkNeeded(installed: Table[string, Installed], - name: string, version: string, downgrade: bool): tuple[needed: bool, vercmp: int] = - if installed.hasKey(name): +func checkNeeded(installed: Table[string, Installed]; + name, version: string; downgrade: bool): tuple[needed: bool, vercmp: int] = + if name in installed: let i = installed[name] - let vercmp = vercmp(version, cstring(i.version)) - let needed = if downgrade: vercmp != 0 else: vercmp > 0 - (needed, vercmp.int) + let vc = vercmp(version, cstring(i.version)) + let needed = if downgrade: vc != 0 else: vc > 0 + (needed, vc.int) else: (true, 0) -proc obtainAurPackageInfos(config: Config, rpcInfos: seq[RpcPackageInfo], - rpcAurTargets: seq[FullPackageTarget], installed: Table[string, Installed], - printMode: bool, noconfirm: bool, needed: bool, upgradeCount: int): (seq[PackageInfo], seq[PackageInfo], - seq[string], seq[Installed], seq[LocalIsNewer], seq[string]) = - let targetRpcInfoPairs: seq[tuple[rpcInfo: RpcPackageInfo, upgradeable: bool]] = - rpcAurTargets.map(f => f.rpcInfo.get).map(i => (i, installed - .checkNeeded(i.name, i.version, true).needed)) - - let upToDateNeeded: seq[Installed] = if needed: - targetRpcInfoPairs.map(pair => (block: - if not pair.upgradeable: - some(installed[pair.rpcInfo.name]) - else: - none(Installed))) - .filter(i => i.isSome) - .map(i => i.unsafeGet) +proc inputLoop(config: Config; reqUpgrades: openArray[Upgrade]; + noconfirm: bool): seq[RpcPackageInfo] = + while true: + let input = printColonUserInput(config.color, + tr"Packages to skip (syntax: 1, 3-5, 7 11)" & ":", noconfirm, "", "") + let intervalsOpt = parseNumberIntervals(input, reqUpgrades.len) + if intervalsOpt.isSome: + let intervals = intervalsOpt.unsafeGet + var filtered: seq[RpcPackageInfo] + for idx, i in reqUpgrades: + if not intervals.anyIt(idx + 1 in it): + filtered.add i.rpcInfo + return filtered + printError(config.color, tr"invalid package selection") + +proc obtainAurPackageInfos(config: Config; rpcInfos: seq[RpcPackageInfo]; + rpcAurTargets: seq[FullPackageTarget]; installed: Table[string, Installed]; + printMode, noconfirm, needed: bool; upgradeCount: int): AurPackageInfosRes = + let (upToDateNeeded, targetRpcInfos, targetRefs) = block: + var skipped: seq[Installed] + var infos: seq[RpcPackageInfo] + var refs: seq[PackageReference] + + for target in rpcAurTargets: + doAssert target.rpcInfo.isSome, "obtainAurPackageInfos requires resolved AUR rpcInfo" + let rpcInfo = target.rpcInfo.get + let upgradeable = installed.checkNeeded(rpcInfo.name, rpcInfo.version, true).needed + if needed and not upgradeable: + skipped.add installed[rpcInfo.name] + if not needed or upgradeable: + infos.add rpcInfo + refs.add target.sync.target.reference + (skipped, infos, refs) + + let (upgradeStructs, localIsNewerSeq) = block: + var upgrades: seq[Upgrade] + var localNewer: seq[LocalIsNewer] + + if upgradeCount > 0: + for i in rpcInfos: + let reference = i.toPackageReference + if targetRefs.anyIt(it.isProvidedBy(reference, true)): + continue + let checkRes = installed.checkNeeded(i.name, i.version, upgradeCount >= 2) + let (newNeeded, localIsNewer) = + if i.name.isVcs: + (installed.checkNeeded(i.name, i.version, false).needed, none(LocalIsNewer)) + elif not checkRes.needed and checkRes.vercmp < 0: + (checkRes.needed, some(LocalIsNewer(name: i.name, + version: installed[i.name].version, aurVersion: i.version))) + else: + (checkRes.needed, none(LocalIsNewer)) + for lin in localIsNewer: + localNewer.add lin + upgrades.add Upgrade(rpcInfo: i, needed: newNeeded) + (upgrades, localNewer) + + var reqUpgrades, ignoredUpgrades: seq[Upgrade] + for u in upgradeStructs: + if not u.needed: continue + if config.ignored(u.rpcInfo.name, installed[u.rpcInfo.name].groups): + ignoredUpgrades.add u else: - @[] - - let installedUpgradeRpcInfos = rpcInfos.filter(i => upgradeCount > 0 and (block: - let reference = i.toPackageReference - rpcAurTargets.filter(f => f.sync.target.reference.isProvidedBy(reference, true)).len == 0)) - - type Upgrade = tuple[ - rpcInfo: RpcPackageInfo, - needed: bool, - localIsNewer: Option[LocalIsNewer] - ] - let upgradeStructs: seq[Upgrade] = installedUpgradeRpcInfos.mapIt: - let res = installed.checkNeeded(it.name, it.version, upgradeCount >= 2) - let (newNeeded, localIsNewer) = - if it.name.isVcs: - # Don't warn about newer local git packages and don't downgrade them - (installed.checkNeeded(it.name, it.version, false).needed, none(LocalIsNewer)) - elif not res.needed and res.vercmp < 0: - (res.needed, some((it.name, installed[it.name].version, it.version))) - else: - (res.needed, none(LocalIsNewer)) - (it, newNeeded, localIsNewer) - - let (reqUpgrades, ignoredUpgrades) = block: - var selectable, ignored: seq[Upgrade] - for u in upgradeStructs: - if u.needed: - if config.ignored(u.rpcInfo.name, installed[u.rpcInfo.name].groups): - ignored.add u - else: - selectable.add u - (selectable, ignored) + reqUpgrades.add u for upgrade in ignoredUpgrades: - let installedVersion = installed[upgrade.rpcInfo.name].version - let newVersion = upgrade.rpcInfo.version - let warnStr = if vercmp(newVersion.cstring, installedVersion.cstring) < 0: + let instVer = installed[upgrade.rpcInfo.name].version + let newVer = upgrade.rpcInfo.version + let warnFmt = if vercmp(newVer.cstring, instVer.cstring) < 0: tra("%s: ignoring package downgrade (%s => %s)\n") else: tra("%s: ignoring package upgrade (%s => %s)\n") - printWarning(config.color, warnStr % - [upgrade.rpcInfo.name, installedVersion, newVersion]) + printWarning(config.color, warnFmt % [upgrade.rpcInfo.name, instVer, newVer]) let selectedUpgradeRpcInfos = - if printMode or noconfirm: - reqUpgrades.mapIt(it.rpcInfo) + if printMode or noconfirm: reqUpgrades.mapIt(it.rpcInfo) + elif reqUpgrades.len == 0: @[] else: - if reqUpgrades.len == 0: @[] - else: - printColon(config.color, tr"Available AUR upgrades") - echo() - let numberWidth = max(($reqUpgrades.len).len, 2) - for index, upgrade in reqUpgrades: - let installedVersion = installed[upgrade.rpcInfo.name].version - let number = align($(index + 1), numberWidth, ' ') - echo(number, ") ", config.aurRepo, "/", upgrade.rpcInfo.name, - " ", installedVersion, " -> ", upgrade.rpcInfo.version) - echo() - - proc inputLoop(): seq[RpcPackageInfo] = - while true: - let input = printColonUserInput(config.color, - tr"Packages to skip (syntax: 1, 3-5, 7 11)" & ":", noconfirm, "", "") - let intervalsOpt = parseNumberIntervals(input, reqUpgrades.len) - if intervalsOpt.isSome: - let intervals = intervalsOpt.unsafeGet - return block: - var filtered: seq[RpcPackageInfo] - for idx, i in reqUpgrades: - if not intervals.anyIt(idx + 1 in it): - filtered.add i.rpcInfo - filtered - printError(config.color, tr"invalid package selection") - inputLoop() - - let targetRpcInfos = targetRpcInfoPairs - .filter(i => not needed or i.upgradeable).map(i => i.rpcInfo) - let upgradeRpcInfos = selectedUpgradeRpcInfos - let fullRpcInfos = targetRpcInfos & upgradeRpcInfos - - let (update, terminate) = createCloneProgress(config, fullRpcInfos.len, true, printMode) - - let (pkgInfos, additionalPkgInfos, paths, errors) = if printMode: (block: - let (pkgInfos, additionalPkgInfos, aerrors) = getAurPackageInfos(fullRpcInfos - .map(i => i.name), config.aurRepo, config.common.arch, config.common.downloadTimeout, config.color) - (pkgInfos, additionalPkgInfos, newSeq[string](), aerrors.deduplicate)) - else: (block: - let (rpcInfos, aerrors) = getRpcPackageInfos(fullRpcInfos.map(i => i.name), - config.aurRepo, config.common.downloadTimeout, config.color) - let (pkgInfos, additionalPkgInfos, paths, cerrors) = - cloneAurReposWithPackageInfos(config, rpcInfos, not printMode, update, true) - (pkgInfos, additionalPkgInfos, paths, (toSeq(aerrors.items) & cerrors).deduplicate)) - - terminate() - - let localIsNewerSeq = upgradeStructs - .filter(p => p.localIsNewer.isSome) - .map(p => p.localIsNewer.unsafeGet) - - (pkgInfos, additionalPkgInfos, paths, upToDateNeeded, localIsNewerSeq, errors) - -proc obtainPacmanBuildTargets(config: Config, pacmanTargets: seq[FullPackageTarget], - installedTable: Table[string, Installed], printMode: bool, needed: bool, build: bool): - (bool, seq[PackageInfo], seq[(string, string)], seq[string], seq[string]) = - let (neededPacmanBuildTargets, buildUpToDateNeeded) = if not printMode and - build and needed: (block: - let neededPairs: seq[tuple[full: FullPackageTarget, - skipVersion: Option[string]]] = pacmanTargets.map(full => (block: - let version = full.sync.foundInfos[0].pkg.get.version - if installedTable.checkNeeded(full.sync.target.reference.name, version, true).needed: - (full, none(string)) + printColon(config.color, tr"Available AUR upgrades") + echo() + let numberWidth = max(($reqUpgrades.len).len, 2) + for index, upgrade in reqUpgrades: + let instVer = installed[upgrade.rpcInfo.name].version + echo(align($(index + 1), numberWidth, ' '), ") ", config.aurRepo, "/", + upgrade.rpcInfo.name, " ", instVer, " -> ", upgrade.rpcInfo.version) + echo() + inputLoop(config, reqUpgrades, noconfirm) + + let fullRpcInfos = block: + var res = targetRpcInfos + for u in selectedUpgradeRpcInfos: + res.add u + res + + withCloneProgress(config, fullRpcInfos.len, true, printMode, cloneUpdate): + let fetched = fetchAurInfos(config, rpcNames(fullRpcInfos), printMode, cloneUpdate) + result = AurPackageInfosRes( + pkgInfos: fetched.pkgInfos, + additionalPkgInfos: fetched.additionalPkgInfos, + paths: fetched.paths, + upToDateNeeded: upToDateNeeded, + localIsNewerSeq: localIsNewerSeq, + errors: fetched.errors) + +proc obtainPacmanBuildTargets(config: Config; pacmanTargets: seq[FullPackageTarget]; + installedTable: Table[string, Installed]; printMode, needed, build: bool): + PacmanBuildTargetsRes = + let (neededTargets, buildUpToDateNeeded) = block: + var targets: seq[FullPackageTarget] + var skipped: seq[(string, string)] + + if not printMode and build and needed: + for target in pacmanTargets: + assert target.sync.foundInfos.len > 0, + "obtainPacmanBuildTargets requires sync package info" + assert target.sync.foundInfos[0].pkg.isSome, + "obtainPacmanBuildTargets requires pacman package info" + let version = target.sync.foundInfos[0].pkg.get.version + if installedTable.checkNeeded(target.sync.target.reference.name, version, true).needed: + targets.add target else: - (full, some(version)))) - - let neededPacmanBuildTargets = neededPairs - .filter(p => p.skipVersion.isNone) - .map(p => p.full) - - let buildUpToDateNeeded = neededPairs - .filter(p => p.skipVersion.isSome) - .map(p => (p.full.sync.target.reference.name, p.skipVersion.unsafeGet)) - - (neededPacmanBuildTargets, buildUpToDateNeeded)) + skipped.add (target.sync.target.reference.name, version) else: - (pacmanTargets, @[]) - - let checkPacmanBuildPkgInfos = not printMode and build and neededPacmanBuildTargets.len > 0 - - let (buildPkgInfos, buildPaths, obtainErrorMessages) = if checkPacmanBuildPkgInfos: (block: - echo(tr"checking official repositories...") - let (update, terminate) = createCloneProgress(config, pacmanTargets.len, false, printMode) - let res = obtainBuildPkgInfos(config, pacmanTargets, update, true) - terminate() - res) - else: - (@[], @[], @[]) - - (checkPacmanBuildPkgInfos, buildPkgInfos, buildUpToDateNeeded, buildPaths, obtainErrorMessages) + targets = pacmanTargets + (targets, skipped) + + if printMode or not build or neededTargets.len == 0: + return PacmanBuildTargetsRes(upToDateNeeded: buildUpToDateNeeded) + + echo(tr"checking official repositories...") + withCloneProgress(config, neededTargets.len, false, printMode, cloneUpdate): + let (pkgInfos, paths, errors) = obtainBuildPkgInfos(config, neededTargets, cloneUpdate, true) + result = PacmanBuildTargetsRes( + checked: true, + pkgInfos: pkgInfos, + upToDateNeeded: buildUpToDateNeeded, + paths: paths, + errors: errors) + +func createInstalled(dbs: seq[ptr AlpmDatabase]; package: ptr AlpmPackage): Installed = + let foreign = block: + var found = false + for db in dbs: + if db[package.name] != nil: + found = true + break + not found + Installed(name: $package.name, version: $package.version, + groups: package.groups.toSeq(), explicit: package.reason == AlpmReason.explicit, + foreign: foreign) proc obtainInstalledWithAur(config: Config): (seq[Installed], seq[string]) = withAlpmConfig(config, true, handle, dbs, errors): for e in errors: printError(config.color, e) - - proc createInstalled(package: ptr AlpmPackage): Installed = - let foreign = dbs.filter(d => d[package.name] != nil).len == 0 - ($package.name, $package.version, package.groupsSeq, - package.reason == AlpmReason.explicit, foreign) - - let installed = collect(newSeq): + let installed = block: + var res: seq[Installed] for p in handle.local.packages: - createInstalled(p) - let checkAurUpgradeNames = installed - .filter(i => i.foreign and (config.checkIgnored or not config.ignored(i.name, i.groups))) - .map(i => i.name) - + res.add createInstalled(dbs, p) + res + let checkAurUpgradeNames = block: + var res: seq[string] + for inst in installed: + if inst.foreign and (config.checkIgnored or not config.ignored(inst.name, inst.groups)): + res.add inst.name + res (installed, checkAurUpgradeNames) -proc resolveBuildTargets(config: Config, syncTargets: seq[SyncPackageTarget], - fullTargets: seq[FullPackageTarget], printHeader: bool, - printMode: bool, upgradeCount: int, noconfirm: bool, needed: bool, noaur: bool, build: bool): - (int, seq[Installed], HashSet[string], seq[PackageInfo], seq[PackageInfo], seq[string]) = - template errorResult: untyped = (1, newSeq[Installed](), initHashSet[string](), - newSeq[PackageInfo](), newSeq[PackageInfo](), newSeq[string]()) +proc resolveBuildTargets(config: Config; syncTargets: seq[SyncPackageTarget]; + fullTargets: seq[FullPackageTarget]; printHeader, printMode: bool; + upgradeCount: int; noconfirm, needed, noaur, build: bool): BuildTargetsRes = + template errorResult: BuildTargetsRes = BuildTargetsRes(code: 1) let (installed, checkAurUpgradeNames) = obtainInstalledWithAur(config) let checkAur = not noaur and checkAurUpgradeNames.len > 0 and upgradeCount > 0 @@ -1235,287 +1615,331 @@ proc resolveBuildTargets(config: Config, syncTargets: seq[SyncPackageTarget], if not printMode and (checkAur or build) and printHeader: printColon(config.color, tr"Resolving build targets...") - let upgradeRpcInfos = if checkAur: (block: - if not printMode: - echo(tr"checking AUR database for upgrades...") - let (upgradeRpcInfos, rerrors) = getRpcPackageInfos(checkAurUpgradeNames, + let upgradeRpcInfos = + if checkAur: + if not printMode: echo(tr"checking AUR database for upgrades...") + let (infos, rerrors) = getRpcPackageInfos(checkAurUpgradeNames, config.aurRepo, config.common.downloadTimeout, config.color) for e in rerrors: printError(config.color, e) - upgradeRpcInfos) + infos else: @[] - let installedTable = installed.map(i => (i.name, i)).toTable - let rpcAurTargets = fullTargets.filter(f => f.isAurTargetFull(config.aurRepo)) - - let targetRpcInfos = collect(newSeq): - for t in rpcAurTargets: - for x in t.rpcInfo: - x - let targetRpcInfoNames = targetRpcInfos.map(i => i.name).toHashSet - let rpcInfos = targetRpcInfos & upgradeRpcInfos.filter(i => not (i.name in targetRpcInfoNames)) - - let (aurPkgInfos, additionalPkgInfos, aurPaths, upToDateNeeded, localIsNewerSeq, aperrors) = - obtainAurPackageInfos(config, rpcInfos, rpcAurTargets, installedTable, - printMode, noconfirm, needed, upgradeCount) - for e in aperrors: printError(config.color, e) + let installedTable = installed.mapIt((it.name, it)).toTable + let (rpcAurTargets, rpcInfos) = block: + var aurTargets: seq[FullPackageTarget] + var infos: seq[RpcPackageInfo] + var infoNames = initHashSet[string]() + + for target in fullTargets: + if target.isAurTargetFull(config.aurRepo): + aurTargets.add target + for info in target.rpcInfo: + infos.add info + infoNames.incl info.name + for info in upgradeRpcInfos: + if info.name notin infoNames: + infos.add info + (aurTargets, infos) + + let aur = obtainAurPackageInfos(config, rpcInfos, rpcAurTargets, installedTable, + printMode, noconfirm, needed, upgradeCount) + for e in aur.errors: printError(config.color, e) + + let upToDateNeededTable: Table[string, PackageReference] = aur.upToDateNeeded + .mapIt((it.name, (it.name, none(string), + some((ConstraintOperation.eq, it.version, false))))).toTable - let upToDateNeededTable: Table[string, PackageReference] = upToDateNeeded.map(i => (i.name, - (i.name, none(string), some((ConstraintOperation.eq, i.version, false))))).toTable let notFoundTargets = filterNotFoundSyncTargets(syncTargets, - aurPkgInfos.map(p => p.rpc), upToDateNeededTable, config.aurRepo) + aur.pkgInfos.mapIt(it.rpc), upToDateNeededTable, config.aurRepo) if notFoundTargets.len > 0: - clearPaths(aurPaths) + # Pre-build exit: only worktrees exist, no archives to remove. + clearWorktrees(config, aur.paths, + CleanupSpec(removeWorktrees: true, removeArchives: false, nukeTmpPrefix: true)) printSyncNotFound(config, notFoundTargets) - errorResult - else: - let fullTargets = mapAurTargets(syncTargets - .filter(s => not (upToDateNeededTable.opt(s.target.reference.name) - .map(r => s.target.reference.isProvidedBy(r, true)).get(false))), - aurPkgInfos.map(p => p.rpc), config.aurRepo) - let pacmanTargets = fullTargets.filter(f => not f.isAurTargetFull(config.aurRepo)) - let aurTargets = fullTargets.filter(f => f.isAurTargetFull(config.aurRepo)) - - let (checkPacmanBuildPkgInfos, buildPkgInfos, buildUpToDateNeeded, buildPaths, - obtainBuildErrorMessages) = obtainPacmanBuildTargets(config, pacmanTargets, installedTable, - printMode, needed, build) - - if checkPacmanBuildPkgInfos and buildPkgInfos.len < pacmanTargets.len: - clearPaths(buildPaths) - clearPaths(aurPaths) - for e in obtainBuildErrorMessages: printError(config.color, e) - errorResult - else: - let pkgInfos = (buildPkgInfos & aurPkgInfos) - .deduplicatePkgInfos(config, not printMode) - let targetNamesSet = (pacmanTargets & aurTargets) - .map(f => f.sync.target.reference.name).toHashSet - let (finalPkgInfos, acceptedPkgInfos) = filterIgnoresAndConflicts(config, pkgInfos, - targetNamesSet, installedTable, printMode, noconfirm) - - if not printMode: - printAllWarnings(config, installed, rpcInfos, - pkgInfos, acceptedPkgInfos, upToDateNeeded, buildUpToDateNeeded, - localIsNewerSeq, targetNamesSet, upgradeCount, noaur) - - (0, installed, targetNamesSet, finalPkgInfos, additionalPkgInfos, buildPaths & aurPaths) - -proc assumeInstalled(args: seq[Argument]): seq[PackageReference] = - args - .filter(a => a.matchOption(%%%"assume-installed")) - .map(a => a.value.get.parsePackageReference(false)) - .filter(r => r.constraint.isNone or - r.constraint.unsafeGet.operation == ConstraintOperation.eq) - -proc handleInstall(args: seq[Argument], config: Config, syncTargets: seq[SyncPackageTarget], - fullTargets: seq[FullPackageTarget], upgradeCount: int, nodepsCount: int, - wrapUpgrade: bool, noconfirm: bool, needed: bool, build: bool, noaur: bool): int = - let pacmanTargets = fullTargets.filter(f => not f.isAurTargetFull(config.aurRepo)) - - let workDirectPacmanTargets = if build: @[] else: pacmanTargets.map(f => $f.sync.target) + return errorResult + + let remappedTargets = mapAurTargets((block: + var targets: seq[SyncPackageTarget] + for target in syncTargets: + let targetRef = target.target.reference + if not upToDateNeededTable.opt(targetRef.name) + .map(r => targetRef.isProvidedBy(r, true)).get(false): + targets.add target + targets), aur.pkgInfos.mapIt(it.rpc), config.aurRepo) + + let (pacmanTargets, targetNamesSet) = block: + var pacmanTargets: seq[FullPackageTarget] + var targetNames = initHashSet[string]() + + for target in remappedTargets: + targetNames.incl target.sync.target.reference.name + if not target.isAurTargetFull(config.aurRepo): + pacmanTargets.add target + (pacmanTargets, targetNames) + + let pacman = obtainPacmanBuildTargets(config, pacmanTargets, installedTable, + printMode, needed, build) + + if pacman.checked and pacman.pkgInfos.len < pacmanTargets.len: + # Pre-build exit: only worktrees exist, no archives to remove. + clearWorktrees(config, pacman.paths & aur.paths, + CleanupSpec(removeWorktrees: true, removeArchives: false, nukeTmpPrefix: true)) + for e in pacman.errors: printError(config.color, e) + return errorResult + + let pkgInfos = (pacman.pkgInfos & aur.pkgInfos).deduplicatePkgInfos(config, not printMode) + let (finalPkgInfos, acceptedPkgInfos) = filterIgnoresAndConflicts(config, pkgInfos, + targetNamesSet, installedTable, printMode, noconfirm) + + if not printMode: + printAllWarnings(config, installed, installedTable, rpcInfos, + pkgInfos, acceptedPkgInfos, aur.upToDateNeeded, pacman.upToDateNeeded, + aur.localIsNewerSeq, targetNamesSet, upgradeCount, noaur) + + BuildTargetsRes( + installed: installed, + targetNamesSet: targetNamesSet, + pkgInfos: finalPkgInfos, + additionalPkgInfos: aur.additionalPkgInfos, + paths: pacman.paths & aur.paths) + +func assumeInstalled(args: openArray[Argument]): seq[PackageReference] = + for i in args: + if i.matchOption(%%%"assume-installed"): + let packRef = i.value.get.parsePackageReference(false) + if (packRef.constraint.isNone or + packRef.constraint.unsafeGet.operation == ConstraintOperation.eq): + result.add packRef + + +proc handleInstall(args: seq[Argument]; config: Config; + syncTargets: seq[SyncPackageTarget]; fullTargets: seq[FullPackageTarget]; + upgradeCount, nodepsCount: int; wrapUpgrade, noconfirm, needed, build, noaur: bool): int = + let pacmanTargets = fullTargets.filterIt(not it.isAurTargetFull(config.aurRepo)) + let workDirectPacmanTargets = if build: @[] else: pacmanTargets.mapIt($it.sync.target) # check for sysupgrade instead of upgradeCount since upgrade could be done before # and then removed from the list of arguments - let (directCode, directSome) = if workDirectPacmanTargets.len > 0 or args.check(%%%"sysupgrade"): - (pacmanRun(some config.sudoCommand, config.color, args.filter(arg => not arg.isTarget) & - workDirectPacmanTargets.map(t => (t, none(string), ArgumentType.target))), true) + let (directCode, directSome) = + if workDirectPacmanTargets.len > 0 or args.check(%%%"sysupgrade"): + (pacmanRun(some config.sudoCommand, config.color, + args.filterIt(not it.isTarget) & + workDirectPacmanTargets.targetArguments), true) else: (0, false) if directCode != 0: - directCode - else: - let (resolveTargetsCode, installed, targetNamesSet, pkgInfos, additionalPkgInfos, - initialPaths) = resolveBuildTargets(config, syncTargets, fullTargets, - directSome or wrapUpgrade, false, upgradeCount, noconfirm, needed, noaur, build) - - if resolveTargetsCode != 0: - removeTmpDirQuiet(config.tmpRootCurrent) - resolveTargetsCode + return directCode + + let resolved = resolveBuildTargets(config, syncTargets, fullTargets, + directSome or wrapUpgrade, false, upgradeCount, noconfirm, needed, noaur, build) + + if resolved.code != 0: + removeTmpDirQuiet(config.tmpRootCurrent) + return resolved.code + + let assumeInst = args.assumeInstalled + let skipDeps = assumeInst.len > 0 or nodepsCount > 0 + + let (_, satisfied, additionalPacmanTargets, basePackages, dependencyPaths) = + resolveDependencies(config, resolved.pkgInfos, resolved.additionalPkgInfos, false, + nodepsCount, assumeInst, noaur) + + let paths = resolved.paths & dependencyPaths + + # Spec used for early-exit cleanup (user abort, dep failure) + # worktrees survive only when requested via `KeepBuildDirOnFailure`. + let archivesOutsideTmp = config.packageOutputDir.len > 0 + let earlyFailSpec = CleanupSpec( + removeWorktrees: not config.keepBuildDirOnFailure, + removeArchives: true, + nukeTmpPrefix: not config.keepBuildDirOnFailure and archivesOutsideTmp) + + let confirmCode = confirmViewAndImportKeys(config, basePackages, resolved.installed, noconfirm) + if confirmCode != 0: + clearWorktrees(config, paths, earlyFailSpec) + return confirmCode + + let (explicitsNamesSet, depsNamesSet) = block: + var expl, deps: HashSet[string] + for installedPack in resolved.installed: + let name = installedPack.name + if installedPack.explicit: expl.incl name else: deps.incl name + (expl, deps) + let keepNames = explicitsNamesSet + depsNamesSet + resolved.targetNamesSet + + let explicits = + if args.check(%%%"asexplicit"): + keepNames # targetNamesSet + explicitsNamesSet + depsNamesSet + elif args.check(%%%"asdeps"): + initHashSet[string]() else: - let assumeInstalled = args.assumeInstalled - let skipDeps = assumeInstalled.len > 0 or nodepsCount > 0 - - let (_, satisfied, additionalPacmanTargets, basePackages, dependencyPaths) = - resolveDependencies(config, pkgInfos, additionalPkgInfos, false, - nodepsCount, assumeInstalled, noaur) - - let confirmAndResolveCode = confirmViewAndImportKeys(config, basePackages, installed, noconfirm) - let paths = initialPaths & dependencyPaths - if confirmAndResolveCode != 0: - clearPaths(paths, true) - confirmAndResolveCode - else: - let explicitsNamesSet = installed.filter(i => i.explicit).map(i => i.name).toHashSet - let depsNamesSet = installed.filter(i => not i.explicit).map(i => i.name).toHashSet - let keepNames = explicitsNamesSet + depsNamesSet + targetNamesSet - - let explicits = if args.check(%%%"asexplicit"): - targetNamesSet + explicitsNamesSet + depsNamesSet - elif args.check(%%%"asdeps"): - initHashSet[string]() - else: - explicitsNamesSet + (targetNamesSet - depsNamesSet) - - let commonArgs = args - .keepOnlyOptions(commonOptions, transactionOptions, upgradeOptions) - .filter(true, false, %%%"asdeps", %%%"asexplicit", %%%"needed") - - let (_, initialUnrequired, initialUnrequiredWithoutOptional, _) = - withAlpmConfig(config, false, handle, dbs, errors): - queryUnrequired(handle, true, true, keepNames) - - let additionalCode = if additionalPacmanTargets.len > 0: (block: - printColon(config.color, tr"Installing build dependencies...") + explicitsNamesSet + (resolved.targetNamesSet - depsNamesSet) + + let commonArgs = args + .keepOnlyOptions(commonOptions, transactionOptions, upgradeOptions) + .filter(true, false, %%%"asdeps", %%%"asexplicit", %%%"needed") + + let (_, initialUnrequired, initialUnrequiredWithoutOptional, _) = + withAlpmConfig(config, false, handle, dbs, errors): + queryUnrequired(handle, true, true, keepNames) + + if additionalPacmanTargets.len > 0: + printColon(config.color, tr"Installing build dependencies...") + let addCode = pacmanRun(some config.sudoCommand, config.color, commonArgs & + ("S", none(string), ArgumentType.short) & + ("needed", none(string), ArgumentType.long) & + ("asdeps", none(string), ArgumentType.long) & + additionalPacmanTargets.targetArguments) + if addCode != 0: + clearWorktrees(config, paths, earlyFailSpec) + return addCode + + if basePackages.len == 0: + let aurTargets = fullTargets.filterIt(it.isAurTargetFull(config.aurRepo)) + if (not noaur and (aurTargets.len > 0 or upgradeCount > 0)) or build: + echo(trp(" there is nothing to do\n")) + clearWorktrees(config, paths, + CleanupSpec(removeWorktrees: true, removeArchives: true, nukeTmpPrefix: true)) + return 0 + + # check all pacman dependencies were installed + let unsatisfied = + if nodepsCount <= 1: + withAlpmConfig(config, true, handle, dbs, errors): + for e in errors: printError(config.color, e) + collect(newSeq): + for x in satisfied.namedPairs: + if not x.value.installed and x.value.buildPkgInfo.isNone and + not checkSatisfied(handle, nodepsCount, x.key): + x.key + else: + @[] - pacmanRun(some config.sudoCommand, 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)))) - else: - 0 + if unsatisfied.len > 0: + clearWorktrees(config, paths, earlyFailSpec) + printUnsatisfied(config, satisfied, unsatisfied) + return 1 - if additionalCode != 0: - clearPaths(paths, true) - additionalCode - else: - if basePackages.len > 0: - # check all pacman dependencies were installed - let unsatisfied = if nodepsCount <= 1: - withAlpmConfig(config, true, 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.toPackageReference, nodepsCount == 0): - return true - for provides in pkg.provides: - if reference.isProvidedBy(provides.toPackageReference, nodepsCount == 0): - return true - return false - - collect(newSeq): - for x in satisfied.namedPairs: - if not x.value.installed and x.value.buildPkgInfo.isNone and - not x.key.checkSatisfied: - x.key - else: - @[] - - if unsatisfied.len > 0: - clearPaths(paths, true) - printUnsatisfied(config, satisfied, unsatisfied) - 1 - else: - proc installNext(index: int, installedAs: List[(string, string)], - lastCode: int, keepBuiltArtifacts: bool): (Table[string, string], int, int, bool) = - if index < basePackages.len and lastCode == 0: - let installResult = installGroupFromSources(config, commonArgs, - basePackages[index], explicits, skipDeps, noconfirm) - installNext(index + 1, installResult.installedAs ^& installedAs, - installResult.code, installResult.keepBuiltArtifacts) - else: - (toSeq(installedAs.items).toTable, lastCode, index - 1, keepBuiltArtifacts) - - let (installedAs, code, index, keepBuiltArtifacts) = installNext(0, nil, 0, false) - if code != 0 and index < basePackages.len - 1: - printWarning(config.color, tr"installation aborted") - clearPaths(paths, tmpDir = not keepBuiltArtifacts) - - let newKeepNames = keepNames.map(n => installedAs.opt(n).get(n)) - let (_, finalUnrequired, finalUnrequiredWithoutOptional, _) = - withAlpmConfig(config, false, handle, dbs, errors): - queryUnrequired(handle, true, true, newKeepNames) - - let unrequired = finalUnrequired - initialUnrequired - let unrequiredOptional = finalUnrequiredWithoutOptional - - initialUnrequiredWithoutOptional - unrequired - - let removeCode = removeBuildDependencies(config, - commonArgs, unrequired, unrequiredOptional) - if removeCode != 0: - removeCode - else: - code - else: - let aurTargets = fullTargets.filter(f => f.isAurTargetFull(config.aurRepo)) - if (not noaur and (aurTargets.len > 0 or upgradeCount > 0)) or build: - echo(trp(" there is nothing to do\n")) - clearPaths(paths, true) - 0 - -proc handlePrint(args: seq[Argument], config: Config, syncTargets: seq[SyncPackageTarget], - fullTargets: seq[FullPackageTarget], upgradeCount: int, nodepsCount: int, - needed: bool, build: bool, noaur: bool, printFormat: string): int = - let pacmanTargets = fullTargets.filter(f => not f.isAurTargetFull(config.aurRepo)) - let directPacmanTargets = pacmanTargets.map(f => $f.sync.target) - - let (resolveTargetsCode, _, _, pkgInfos, additionalPkgInfos, _) = resolveBuildTargets(config, + let (confFileOpt, confError) = resolveMakepkgConf() + if confFileOpt.isNone: + if confError.len > 0: printError(config.color, confError) + clearWorktrees(config, paths, + CleanupSpec(removeWorktrees: true, removeArchives: true, nukeTmpPrefix: true)) + return 1 + let confFile = confFileOpt.unsafeGet.string + let effectivePkgdest = resolveEffectivePkgdest(confFile) + + var canonicalRenames = initTable[string, string]() + var code = 0 + var lastIndex = -1 + + for index in 0 ..< basePackages.len: + let installResult = installGroupFromSources(config, commonArgs, + basePackages[index], explicits, skipDeps, noconfirm, confFile, effectivePkgdest) + if installResult.code != 0: + code = installResult.code + lastIndex = index + break + for k, v in installResult.canonicalRenames: canonicalRenames[k] = v + lastIndex = index + + if code != 0 and lastIndex < basePackages.len - 1: + printWarning(config.color, tr"installation aborted") + + # Worktree cleanup policy derived directly from config. + # Archives are already handled per-group inside installGroupFromSources. + let successSpec = cleanupSpec(config.cleanupAfterInstall, archivesOutsideTmp) + let failureSpec = CleanupSpec( + removeWorktrees: not config.keepBuildDirOnFailure, + removeArchives: false, + nukeTmpPrefix: not config.keepBuildDirOnFailure and archivesOutsideTmp) + + let spec = if code == 0: successSpec else: failureSpec + clearWorktrees(config, paths, + CleanupSpec(removeWorktrees: spec.removeWorktrees, + removeArchives: false, + nukeTmpPrefix: spec.nukeTmpPrefix)) + + let newKeepNames = block: + var keep = initHashSet[string]() + for n in keepNames: + keep.incl canonicalRenames.getOrDefault(n, n) + keep + + + + let (_, finalUnrequired, finalUnrequiredWithoutOptional, _) = + withAlpmConfig(config, false, handle, dbs, errors): + queryUnrequired(handle, true, true, newKeepNames) + + let unrequired = finalUnrequired - initialUnrequired + let unrequiredOptional = finalUnrequiredWithoutOptional - + initialUnrequiredWithoutOptional - unrequired + + let removeCode = removeBuildDependencies(config, commonArgs, unrequired, unrequiredOptional) + if removeCode != 0: removeCode else: code + +proc handlePrint(args: seq[Argument]; config: Config; + syncTargets: seq[SyncPackageTarget]; fullTargets: seq[FullPackageTarget]; + upgradeCount, nodepsCount: int; needed, build, noaur: bool; + printFormat: string): int = + let pacmanTargets = fullTargets.filterIt(not it.isAurTargetFull(config.aurRepo)) + let directPacmanTargets = pacmanTargets.mapIt($it.sync.target) + + let resolved = resolveBuildTargets(config, syncTargets, fullTargets, false, true, upgradeCount, true, needed, noaur, build) + if resolved.code != 0: + return resolved.code - if resolveTargetsCode != 0: - resolveTargetsCode - else: - let (resolveSuccess, _, additionalPacmanTargets, basePackages, _) = - resolveDependencies(config, pkgInfos, additionalPkgInfos, true, - nodepsCount, args.assumeInstalled, noaur) - - let code = if directPacmanTargets.len > 0 or - additionalPacmanTargets.len > 0 or upgradeCount > 0: (block: - let callPacmanTargets = if resolveSuccess: - directPacmanTargets & additionalPacmanTargets - else: - directPacmanTargets - - # workaround for a strange nim bug, callPacmanTargets.map(...) breaks main.nim - var callArguments = newSeq[Argument]() - for t in callPacmanTargets: - callArguments &= (t, none(string), ArgumentType.target) + let (resolveSuccess, _, additionalPacmanTargets, basePackages, _) = + resolveDependencies(config, resolved.pkgInfos, resolved.additionalPkgInfos, true, + nodepsCount, args.assumeInstalled, noaur) - let code = pacmanRun(noPrefix, config.color, - args.filter(arg => not arg.isTarget) & callArguments) - - if resolveSuccess: - code + let code = + if directPacmanTargets.len > 0 or additionalPacmanTargets.len > 0 or upgradeCount > 0: + let callPacmanTargets = if resolveSuccess: + directPacmanTargets & additionalPacmanTargets else: - 1) - else: - 0 - - if code == 0: - proc printWithFormat(pkgInfo: PackageInfo) = - echo(printFormat - .replace("%n", pkgInfo.rpc.name) - .replace("%v", pkgInfo.rpc.version) - .replace("%r", config.aurRepo) - .replace("%s", "0") - .replace("%l", pkgInfo.rpc.gitUrl)) - - for installGroup in basePackages: - for pkgInfos in installGroup: - for pkgInfo in pkgInfos: - printWithFormat(pkgInfo) - 0 + directPacmanTargets + let c = pacmanRun(noPrefix, config.color, + args.filterIt(not it.isTarget) & callPacmanTargets.targetArguments) + if resolveSuccess: c else: 1 else: - code + 0 -proc resolveAurTargets(config: Config, targets: seq[PackageTarget], printMode: bool, noaur: bool, - build: bool): (int, seq[SyncPackageTarget], seq[FullPackageTarget]) = + if code != 0: + return code + + for installGroup in basePackages: + for pkgInfos in installGroup: + for pkgInfo in pkgInfos: + echo printFormat.multiReplace( + ("%n", pkgInfo.rpc.name), + ("%v", pkgInfo.rpc.version), + ("%r", config.aurRepo), + ("%s", "0"), + ("%l", pkgInfo.rpc.gitUrl)) + 0 + +proc resolveAurTargets(config: Config; targets: seq[PackageTarget]; + printMode, noaur, build: bool): + (int, seq[SyncPackageTarget], seq[FullPackageTarget]) = let (syncTargets, checkAurTargetNames) = withAlpmConfig(config, true, handle, dbs, errors): for e in errors: printError(config.color, e) findSyncTargets(handle, dbs, targets, config.aurRepo, not build, not build, true) - let rpcInfos = if not noaur and checkAurTargetNames.len > 0: (block: + let rpcInfos = + if not noaur and checkAurTargetNames.len > 0: if not printMode: printColon(config.color, tr"Resolving build targets...") echo(tr"checking AUR database for targets...") - - let (rpcInfos, rerrors) = getRpcPackageInfos(checkAurTargetNames, + let (infos, rerrors) = getRpcPackageInfos(checkAurTargetNames, config.aurRepo, config.common.downloadTimeout, config.color) for e in rerrors: printError(config.color, e) - rpcInfos) + infos else: @[] @@ -1529,10 +1953,11 @@ proc resolveAurTargets(config: Config, targets: seq[PackageTarget], printMode: b let fullTargets = mapAurTargets(syncTargets, rpcInfos, config.aurRepo) (0, syncTargets, fullTargets) -proc handleSyncInstall*(args: seq[Argument], config: Config): int = +proc handleSyncInstall*(args: seq[Argument]; config: Config): int = let printModeArg = args.check(%%%"print") - let printModeFormat = args.filter(arg => arg.matchOption(%%%"print-format")).optLast - let printFormat = if printModeArg or printModeFormat.isSome: + let printModeFormat = args.filterIt(it.matchOption(%%%"print-format")).optLast + let printFormat = + if printModeArg or printModeFormat.isSome: some(printModeFormat.map(arg => arg.value.get).get("%l")) else: none(string) @@ -1540,33 +1965,35 @@ proc handleSyncInstall*(args: seq[Argument], config: Config): int = let targets = args.packageTargets(false) let wrapUpgrade = targets.len == 0 - let (refreshUpgradeCode, callArgs) = if wrapUpgrade and printFormat.isNone: + let (refreshUpgradeCode, callArgs) = + if wrapUpgrade and printFormat.isNone: checkAndRefreshUpgrade(config.sudoCommand, config.color, args) else: checkAndRefresh(config.sudoCommand, config.color, args) if refreshUpgradeCode != 0: - refreshUpgradeCode - else: - let upgradeCount = args.count(%%%"sysupgrade") - let nodepsCount = args.count(%%%"nodeps") - let needed = args.check(%%%"needed") - let noaur = args.check(%%%"noaur") - let build = args.check(%%%"build") - let noconfirm = args.noconfirm - - withAur(): - let (code, syncTargets, fullTargets) = resolveAurTargets(config, targets, - printFormat.isSome, noaur, build) - - let pacmanArgs = callArgs.filterExtensions(true, true, - commonOptions, transactionOptions, upgradeOptions, syncOptions) - - if code != 0: - code - elif printFormat.isSome: - handlePrint(pacmanArgs, config, syncTargets, fullTargets, - upgradeCount, nodepsCount, needed, build, noaur, printFormat.unsafeGet) - else: - handleInstall(pacmanArgs, config, syncTargets, fullTargets, - upgradeCount, nodepsCount, wrapUpgrade, noconfirm, needed, build, noaur) + return refreshUpgradeCode + + let upgradeCount = args.count(%%%"sysupgrade") + let nodepsCount = args.count(%%%"nodeps") + let needed = args.check(%%%"needed") + let noaur = args.check(%%%"noaur") + let build = args.check(%%%"build") + let noconfirm = args.noconfirm + + withAur(): + let (code, syncTargets, fullTargets) = resolveAurTargets(config, targets, + printFormat.isSome, noaur, build) + + let pacmanArgs = callArgs.filterExtensions(true, true, + commonOptions, transactionOptions, upgradeOptions, syncOptions) + + if code != 0: + return code + + if printFormat.isSome: + handlePrint(pacmanArgs, config, syncTargets, fullTargets, + upgradeCount, nodepsCount, needed, build, noaur, printFormat.unsafeGet) + else: + handleInstall(pacmanArgs, config, syncTargets, fullTargets, + upgradeCount, nodepsCount, wrapUpgrade, noconfirm, needed, build, noaur) diff --git a/src/pacman.nim b/src/pacman.nim index e48c200..e2bcab1 100644 --- a/src/pacman.nim +++ b/src/pacman.nim @@ -370,8 +370,25 @@ proc createConfigFromTable(table: Table[string, string], dbs: seq[string]): Pacm raise commandError(tr"can not get the architecture", colorNeeded = some(color.get)) - ((dbs, archFinal, false, true, chomp, verbosePkgLists, downloadTimeout, none(string), true, - ignorePkgs, ignoreGroups), none(string), rootRel, dbRel, cacheRel, gpgRel, color) + PacmanConfig( + common: CommonConfig( + dbs: dbs, + arch: archFinal, + progressBar: true, + chomp: chomp, + verbosePkgLists: verbosePkgLists, + downloadTimeout: downloadTimeout, + defaultRoot: true, + ignorePkgs: ignorePkgs, + ignoreGroups: ignoreGroups, + ), + sysrootOption: none(string), + rootRelOption: rootRel, + dbRelOption: dbRel, + cacheRelOption: cacheRel, + gpgRelOption: gpgRel, + colorMode: color, + ) proc obtainPacmanConfig*(args: seq[Argument]): PacmanConfig = proc getAll(pair: OptionPair): seq[string] = @@ -449,11 +466,27 @@ proc obtainPacmanConfig*(args: seq[Argument]): PacmanConfig = let argsRootRel = rootRel.get("/") let defaultRoot = defaultRootRel == argsRootRel - let config: PacmanConfig = ((defaultConfig.common.dbs, arch, debug, progressBar, defaultConfig.common.chomp, - defaultConfig.common.verbosePkgLists, defaultConfig.common.downloadTimeout and downloadTimeout, - pgpKeyserver, defaultRoot, ignorePkgs + defaultConfig.common.ignorePkgs, - ignoreGroups + defaultConfig.common.ignoreGroups), - sysroot, rootRel, dbRel, cacheRel, gpgRel, color) + let common = block: + var c = defaultConfig.common + c.arch = arch + c.debug = debug + c.progressBar = progressBar + c.downloadTimeout = c.downloadTimeout and downloadTimeout + c.pgpKeyserver = pgpKeyserver + c.defaultRoot = defaultRoot + c.ignorePkgs = ignorePkgs + c.ignorePkgs + c.ignoreGroups = ignoreGroups + c.ignoreGroups + c + + let config: PacmanConfig = PacmanConfig( + common: common, + sysrootOption: sysroot, + rootRelOption: rootRel, + dbRelOption: dbRel, + cacheRelOption: cacheRel, + gpgRelOption: gpgRel, + colorMode: color, + ) pacmanValidateAndThrow((("sysroot", sysroot, ArgumentType.long), sysroot.isSome), (("root", some(config.pacmanRootRel), ArgumentType.long), not defaultRoot),