From 4008e8ec8c30428bf10d94775d0e2d1071fe63b0 Mon Sep 17 00:00:00 2001 From: Nycto Date: Mon, 9 Mar 2026 18:08:58 -0700 Subject: [PATCH 1/3] Use appdirs for directory discovery --- local/percy.nim | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/local/percy.nim b/local/percy.nim index fe9aca7..4079686 100644 --- a/local/percy.nim +++ b/local/percy.nim @@ -1,10 +1,13 @@ import mininim, semver, + std/appdirs, std/re, std/osproc, nimble/parser +from std/paths import Path, `/`, `$` + export mininim, semver, @@ -32,16 +35,13 @@ proc getVendorDir*(subdir: string = ""): string = Get the application's local dir or a subdirectory of it ]# proc getAppLocalDir*(subdir: string = ""): string = - when defined(linux): - result = getHomeDir() / ".local" / "share" / percy.name / subdir - else: - result = getHomeDir() / ("." & percy.name) / subdir + result = $(appdirs.getDataDir() / paths.Path(percy.name) / paths.Path(subdir)) #[ Get the application's cache dir or a subdirectory of it ]# proc getAppCacheDir*(subdir: string = ""): string = - result = percy.getAppLocalDir("cache" / subdir) + result = $(appdirs.getCacheDir() / paths.Path(percy.name) / paths.Path(subdir)) #[ Execute a sequence as a command From b63adec9ef280ff766a3b79a6ddad39bad4f0c66 Mon Sep 17 00:00:00 2001 From: Nycto Date: Sat, 2 May 2026 11:14:09 -0700 Subject: [PATCH 2/3] Link and unlink commands Fixes #1 --- local/commands/link.nim | 55 +++++++++++++++++++++++++++++++++++++++ local/commands/unlink.nim | 48 ++++++++++++++++++++++++++++++++++ local/lib/depgraph.nim | 46 ++++++++++++++++++++++++++++---- local/lib/links.nim | 31 ++++++++++++++++++++++ local/lib/loader.nim | 23 +++++++++++++++- 5 files changed, 197 insertions(+), 6 deletions(-) create mode 100644 local/commands/link.nim create mode 100644 local/commands/unlink.nim create mode 100644 local/lib/links.nim diff --git a/local/commands/link.nim b/local/commands/link.nim new file mode 100644 index 0000000..f6c6b94 --- /dev/null +++ b/local/commands/link.nim @@ -0,0 +1,55 @@ +import + percy, + basecli, + lib/links, + lib/lockfile + +type + LinkCommand = ref object of BaseCommand + +begin LinkCommand: + method execute(console: Console): int = + result = super.execute(console) + + let + name = console.getArg("name") + path = console.getArg("path") + repository = this.settings.getRepository(name) + url = repository.url + targetDir = getVendorDir(this.settings.getWorkDir(url)) + absPath = expandTilde(path).absolutePath() + + let lockFile = LockFile.init(fmt "{percy.name}.lock") + if not lockFile.exists() or not lockFile.commits().anyIt(it.repository.url == url): + fail fmt "'{name}' is not a dependency of this project. Run 'percy install' first if needed." + return 1 + elif not dirExists(absPath): + fail fmt "Path does not exist or is not a directory: '{absPath}'" + return 2 + elif symLinkExists(targetDir): + fail fmt "Already linked. Run `percy unlink {name}` first." + return 3 + + if dirExists(targetDir): + removeDir(targetDir) + + createDir(targetDir.parentDir()) + createSymlink(absPath, targetDir) + + var links = readLinks() + links.links[url] = absPath + writeLinks(links) + + print fmt "Linked '{name}' ({url}) → {absPath}" + +shape LinkCommand: @[ + Command( + name: "link", + description: "Link a local workspace directory as a vendored package", + opts: @[CommandConfigOpt, CommandVerbosityOpt], + args: @[ + Arg(name: "name", description: "Package alias or name to link"), + Arg(name: "path", description: "Local filesystem path to the workspace directory") + ] + ) +] diff --git a/local/commands/unlink.nim b/local/commands/unlink.nim new file mode 100644 index 0000000..0c670c0 --- /dev/null +++ b/local/commands/unlink.nim @@ -0,0 +1,48 @@ +import + percy, + basecli, + lib/links + +type + UnlinkCommand = ref object of BaseCommand + +begin UnlinkCommand: + method execute(console: Console): int = + result = super.execute(console) + + let + name = console.getArg("name") + repository = this.settings.getRepository(name) + url = repository.url + workDir = this.settings.getWorkDir(url) + targetDir = getVendorDir(workDir) + + var links = readLinks() + + if links.links.len == 0: + fail fmt "No linked packages found ('{linksFile}' does not exist)" + return 1 + elif url notin links: + fail fmt "Package '{name}' is not linked" + info fmt "> Hint: Run `percy link {name} ` to link it" + return 2 + + if symLinkExists(targetDir): + removeFile(targetDir) + + links.links.del(url) + writeLinks(links) + + print fmt "Unlinked '{name}'" + info fmt "> Hint: Run `percy install` to restore the vendored version" + +shape UnlinkCommand: @[ + Command( + name: "unlink", + description: "Unlink a workspace directory, restoring normal vendor management", + opts: @[CommandConfigOpt, CommandVerbosityOpt], + args: @[ + Arg(name: "name", description: "Package alias or name to unlink") + ] + ) +] diff --git a/local/lib/depgraph.nim b/local/lib/depgraph.nim index 45a3875..7d5b264 100644 --- a/local/lib/depgraph.nim +++ b/local/lib/depgraph.nim @@ -3,6 +3,7 @@ import semver, lib/settings, lib/repository, + lib/links, mininim/cli export @@ -46,6 +47,7 @@ type tracking: OrderedTable[Repository, OrderedSet[Commit]] requirements: Table[Commit, seq[Requirement]] settings: Settings + links: Links DecisionLevel = int @@ -137,9 +139,12 @@ begin DepGraph: method init*(settings: Settings, quiet: bool = true): void {. base .} = this.quiet = quiet this.settings = settings + this.links = readLinks() method checkConstraint*(requirement: Requirement, commit: Commit): bool {. base .} = - if requirement.constraint.check(commit.version): + if requirement.repository.url in this.links: + result = true + elif requirement.constraint.check(commit.version): result = true elif requirement.constraint.check(ver(commit.id)): result = true @@ -328,8 +333,14 @@ begin DepGraph: print fmt "> Source: {commit.repository.url} @ {commit.version}" try: - for file in commit.repository.listDir("/", commit.id): - if file.endsWith(".nimble"): + if commit.repository.url in this.links: + for file in walkDir(this.links[commit.repository.url]): + if file.path.endsWith(".nimble"): + commit.info = parser.parse(readFile(file.path)) + break + else: + for file in commit.repository.listDir("/", commit.id): + if file.endsWith(".nimble"): let contents = commit.repository.readFile(file, commit.id) when debugging(3): @@ -377,6 +388,29 @@ begin DepGraph: ]# method expandCommits*(requirement: Requirement): void {. base .} = + if requirement.repository.url in this.links: + if not this.commits.hasKey(requirement.repository): + if not this.quiet: + print fmt "Graph: Adding Repository (Using Linked Directory)" + print fmt "> Repository URL: {requirement.repository.url}" + print fmt "> Repository Hash: {requirement.repository.shaHash}" + + var output: string + percy.execIn( + ExecHook as ( + block: + discard percy.execCmdCaptureAll(output, @["git rev-parse HEAD"]) + ), + this.links[requirement.repository.url] + ) + this.commits[requirement.repository] = initOrderedSet[Commit]() + let head = output.strip() + if head.len == 40: + this.commits[requirement.repository].incl( + Commit(id: head, version: ver("head"), repository: requirement.repository) + ) + return + if not this.commits.hasKey(requirement.repository): if requirement.repository.exists: if not this.quiet: @@ -428,15 +462,17 @@ begin DepGraph: toResolve = HashSet[Commit]() toRemove = Table[Commit, string]() + let isLinked = requirement.repository.url in this.links + for commit in this.commits[requirement.repository]: if not this.requirements.hasKey(commit): if depth == 0: - if not this.checkConstraint(requirement, commit): + if not isLinked and not this.checkConstraint(requirement, commit): toRemove[commit] = "Not Usable At Top-Level" else: toResolve.incl(commit) else: - if this.checkConstraint(requirement, commit): + if isLinked or this.checkConstraint(requirement, commit): toResolve.incl(commit) for commit, reason in toRemove: diff --git a/local/lib/links.nim b/local/lib/links.nim new file mode 100644 index 0000000..e3e2b2a --- /dev/null +++ b/local/lib/links.nim @@ -0,0 +1,31 @@ +import + percy + +const + linksFile* = "vendor/links.json" + +type + Links* = ref object of Class + links*: Table[string, string] + +proc `[]`*(links: Links, url: string): string = + links.links[url] + +proc contains*(links: Links, url: string): bool = + links.links.hasKey(url) + +proc readLinks*(): Links = + result = Links() + if fileExists(linksFile): + for k, v in json.parseFile(linksFile)["links"].pairs: + result.links[k] = v.getStr() + +proc writeLinks*(links: Links) = + if links.links.len == 0: + if fileExists(linksFile): + removeFile(linksFile) + else: + var inner = newJObject() + for k, v in links.links: + inner[k] = %v + writeFile(linksFile, pretty(%* { "links": inner })) diff --git a/local/lib/loader.nim b/local/lib/loader.nim index 83397b0..c9666b5 100644 --- a/local/lib/loader.nim +++ b/local/lib/loader.nim @@ -3,6 +3,7 @@ import lib/settings, lib/depgraph, lib/repository, + lib/links, pkg/checksums/sha1 type @@ -216,6 +217,22 @@ begin Loader: if not this.map[relPath]["subs"].contains(%repository.shaHash): this.map[relPath]["subs"].add(%repository.shaHash) + method restoreLinks() {. base .} = + let links = readLinks() + for url, absPath in links.links: + let + workDir = this.settings.getWorkDir(url) + targetDir = getVendorDir(workDir) + if symLinkExists(targetDir): + discard # already fine + elif dirExists(absPath): + createDir(targetDir.parentDir()) + createSymlink(absPath, targetDir) + if not this.quiet: + info fmt "> Restored link: {targetDir} → {absPath}" + elif not this.quiet: + warn fmt "> Broken link for '{url}': '{absPath}' not found (run `percy unlink` to clean up)" + method loadSolution*(solution: Solution, preserve: bool = false, force: bool = false): seq[Checkout] {. base .} = var error: int @@ -227,6 +244,8 @@ begin Loader: createDirs: OrderedSet[string] workTrees: Table[string, WorkTree] + this.restoreLinks() + if not this.quiet: print "Loading Solution" @@ -246,7 +265,9 @@ begin Loader: workTrees = commit.repository.workTrees currentUrl = commit.repository.url - if not workTrees.hasKey(targetDir): + if symLinkExists(targetDir): + retainDirs.incl(targetDir) + elif not workTrees.hasKey(targetDir): if dirExists(targetDir) and not force: info fmt "> Skipped '{targetDir}'" info fmt "> Reason: Working copy is not managed as {currentUrl} (force with -f)" From f31a115e2467b2c9861c9c1c713fb7ec50f2e345 Mon Sep 17 00:00:00 2001 From: "Matthew J. Sahagian" Date: Sun, 28 Jun 2026 12:48:57 -0700 Subject: [PATCH 3/3] Clean up imports in percy.nim Removed unused imports. As written should work with private osappdirs which doesn't rely on explicit Path conversion. Though explicitness may be preferred, keeping convention for now and a more explicit path migration/update across the entire codebase can be done later. --- local/percy.nim | 3 --- 1 file changed, 3 deletions(-) diff --git a/local/percy.nim b/local/percy.nim index 7dd0018..65da406 100644 --- a/local/percy.nim +++ b/local/percy.nim @@ -1,13 +1,10 @@ import mininim, semver, - std/appdirs, std/re, std/osproc, nimble/parser -from std/paths import Path, `/`, `$` - export mininim, semver,