Skip to content

Commit

Permalink
improve API and don't clone every time
Browse files Browse the repository at this point in the history
  • Loading branch information
metagn committed Jan 15, 2022
1 parent 87833e3 commit 29c094f
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 60 deletions.
19 changes: 14 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ A library for installing and importing Nimble packages directly through
Nim code, similar to Groovy's Grape and `@Grab`. Works with NimScript,
as all the computation is done at compile time.

This installs the package globally, and can fairly affect compilation time.
For this reason it should only be used for scripts and snippets and the like.

```nim
import grab
Expand All @@ -12,11 +15,11 @@ grab "regex"
assert "abc.123".match(re"\w+\.\d+")
# run install command with the given arguments (default behavior for string argument as above)
grab package(installCommand = "-Y https://github.com/arnetheduck/nim-result@#HEAD",
name = "result"): # clarify package name
# custom imports from the package directory
results
# run install command with the given arguments
grab package("-y https://github.com/arnetheduck/nim-result@#HEAD",
name = "result", forceInstall = true): # clarify package name to correctly query path
# imports from the package directory
import results
func works(): Result[int, string] =
result.ok(123)
Expand All @@ -27,3 +30,9 @@ func fails(): Result[int, string] =
assert works().isOk
assert fails().error == "abc"
```

Install with:

```
nimble install grab
```
144 changes: 89 additions & 55 deletions src/grab.nim
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ runnableExamples:

assert "abc.123".match(re"\w+\.\d+")

grab package("-Y https://github.com/arnetheduck/nim-result@#HEAD",
name = "result"):
results
grab package("-y https://github.com/arnetheduck/nim-result@#HEAD",
name = "result", forceInstall = true):
import results

func works(): Result[int, string] =
result.ok(42)
Expand All @@ -24,6 +24,11 @@ runnableExamples:

import macros, strutils, os

when not compiles (var x = ""; x.delete(0 .. 0)):
template delete(x: untyped, s: HSlice): untyped =
let y = s
x.delete(y.a, y.b)

proc stripLeft(package: var string) =
package.delete(0 .. max(max(package.rfind('/'), package.rfind('\\')), package.rfind(' ')))

Expand Down Expand Up @@ -53,23 +58,26 @@ proc extractWithVersion(package: string): string =
type Package* = object
## Package information to be used when installing and importing packages.
name*, installCommand*, pathQuery*: string
forceInstall*: bool

proc package*(installCommand, name, pathQuery: string): Package =
proc package*(installCommand, name, pathQuery: string, forceInstall = false): Package =
## Generates package information with arguments to a `nimble install`
## command, package name, and optionally a name and version pair
## for the purpose of querying the module path.
Package(installCommand: installCommand,
name: parseName(name),
pathQuery: pathQuery)
pathQuery: pathQuery,
forceInstall: forceInstall)

proc package*(installCommand, name: string): Package =
proc package*(installCommand, name: string, forceInstall = false): Package =
## Generates package information with arguments to a `nimble install`
## command and a package name (optionally with a version).
Package(installCommand: installCommand,
name: parseName(name),
pathQuery: name)
pathQuery: name,
forceInstall: forceInstall)

proc package*(installCommand: string): Package =
proc package*(installCommand: string, forceInstall = false): Package =
## Converts the arguments of a `nimble install` command into
## package information.
##
Expand All @@ -79,78 +87,104 @@ proc package*(installCommand: string): Package =
## ``package("fakename", "[email protected]")``.
Package(installCommand: installCommand,
pathQuery: extractWithVersion(installCommand),
name: parseName(installCommand))
name: parseName(installCommand),
forceInstall: forceInstall)

proc getPath(package: Package): string =
for line in staticExec("nimble path " & package.pathQuery).splitLines:
if line.len != 0:
result = line

proc grabImpl(package: Package, imports: NimNode): NimNode =
when defined(grabGiveHint):
hint("grabbing: " & package, imports)

let installOutput = staticExec("nimble install -N " & package.installCommand)
if "Error: " in installOutput:
error("could not install " & package.name & ", install log:\p" &
installOutput, imports)
let doPath = package.pathQuery.len != 0
let doInstall = package.forceInstall or
(doPath and not dirExists(getPath(package)))

if doInstall:
let installOutput = staticExec("nimble install " &
(if package.forceInstall: "-Y " else: "-N ") &
package.installCommand)
if "Error: " in installOutput:
error("could not install " & package.name & ", install log:\p" &
installOutput, imports)

let imports =
if imports.len != 0:
imports
else:
let x = ident(package.name)
x.copyLineInfo(imports)
newPar(x)
newStmtList(newTree(nnkImportStmt, x))

proc doImport(p: string, n: NimNode, res: NimNode) =
case n.kind
of nnkPar, nnkBracket, nnkCurly, nnkStmtList, nnkStmtListExpr:
for a in n:
doImport(p, a, res)
else:
var root = n
const replaceKinds = {nnkStrLit..nnkTripleStrLit, nnkIdent, nnkSym, nnkAccQuoted}
proc replace(s: NimNode): NimNode =
if p.len != 0:
var str = (p / $s)
if not str.endsWith(".nim"):
str.add(".nim")
newLit(str)
else:
s
if root.kind in replaceKinds:
res.add(replace root)
else:
while root.len != 0:
let index = if root.kind in {nnkCommand..nnkPostfix}: 1 else: 0
if root[index].kind in replaceKinds:
root[index] = replace root[index]
break
else:
root = root[index]
res.add(n)

let path = staticExec("nimble path " & package.pathQuery).strip
if "Error: " in path:
error("could not get path of " & package.pathQuery & ", got error:\p" &
let path = if doPath: getPath(package) else: ""
if doPath and not dirExists(path):
error("could not locate " & package.pathQuery & ", got error or invalid path:\p" &
path, imports)

result = newNimNode(nnkImportStmt, imports)
for n in imports:
doImport(path, n, result)

macro grab*(package: static Package, imports: varargs[untyped]) =
proc patchImport(p: string, n: NimNode): NimNode =
var root = n
const replaceKinds = {nnkStrLit..nnkTripleStrLit, nnkIdent, nnkSym, nnkAccQuoted}
proc replace(s: NimNode): NimNode =
if p.len != 0:
var str = (p / $s)
if not str.endsWith(".nim"):
str.add(".nim")
newLit(str)
else:
s
if root.kind in replaceKinds:
replace root
else:
while root.len != 0:
let index = if root.kind in {nnkCommand..nnkPostfix}: 1 else: 0
if root[index].kind in replaceKinds:
root[index] = replace root[index]
break
else:
root = root[index]
n

result = copy imports
for imp in result:
case imp.kind
of nnkImportStmt:
for i in 0 ..< imp.len:
imp[i] = patchImport(path, imp[i])
of nnkImportExceptStmt, nnkFromStmt, nnkIncludeStmt:
imp[0] = patchImport(path, imp[0])
else: discard

macro grab*(package: static Package, imports: untyped) =
## Installs a package with Nimble and immediately imports it.
##
## Can be followed with a list of custom imports from the package.
## This can also be an indented block.
## Can be followed with a list of imports from the package in an indented
## block. Imports outside this block will not work. By default, only
## the main module of the package is imported.
##
## This installs the package globally, and can fairly affect compilation time.
## For this reason it should only be used for scripts and snippets and the like.
##
## If the package is already installed, it will not reinstall it.
## This can be overriden by adding `-Y` at the start of the install command.
##
## See module documentation for usage.
let package = package
result = grabImpl(package, imports)

macro grab*(installCommand: static string, imports: varargs[untyped]) =
macro grab*(installCommand: static string, imports: untyped) =
## Shorthand for `grab(package(installCommand), imports)`.
##
## See module documentation for usage.
let installCommand = installCommand
result = grabImpl(package(installCommand), imports)

macro grab*(package) =
## Calls `grab(package, imports)` with the main module
## deduced from the package name imported by default.
let imports = newNilLit()
imports.copyLineInfo(package)
let grabCall = ident("grab")
grabCall.copyLineInfo(package)
result = newCall(grabCall, package, imports)
result.copyLineInfo(package)

0 comments on commit 29c094f

Please sign in to comment.