├── .gitignore ├── tests ├── config.nims ├── test_grab.nim └── test_grab_url_version.nim ├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── grab.nimble ├── README.md └── src └── grab.nim /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.dll 3 | docs/ 4 | -------------------------------------------------------------------------------- /tests/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../src") -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: metagn 2 | custom: https://www.buymeacoffee.com/metagn 3 | -------------------------------------------------------------------------------- /tests/test_grab.nim: -------------------------------------------------------------------------------- 1 | import grab 2 | 3 | grab "assigns" 4 | 5 | block: # test assigns 6 | (a, (b, c)) := (1, (2, 3)) 7 | doAssert (a, b, c) == (1, 2, 3) 8 | -------------------------------------------------------------------------------- /tests/test_grab_url_version.nim: -------------------------------------------------------------------------------- 1 | import grab 2 | 3 | grab "-Y https://github.com/metagn/sliceutils@0.2.0" 4 | 5 | block: # check if we are on exactly sliceutils version 0.2.0 6 | doAssert declared(sliceutils.MultiSlice) 7 | -------------------------------------------------------------------------------- /grab.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.1.1" 4 | author = "metagn" 5 | description = "grab statement for importing Nimble packages, similar to Groovy's Grape" 6 | license = "MIT" 7 | srcDir = "src" 8 | 9 | 10 | # Dependencies 11 | 12 | requires "nim >= 1.0.0" 13 | 14 | when (NimMajor, NimMinor) >= (1, 4): 15 | when (compiles do: import nimbleutils): 16 | import nimbleutils 17 | # https://github.com/metagn/nimbleutils 18 | 19 | task docs, "build docs for all modules": 20 | when declared(buildDocs): 21 | buildDocs(gitUrl = "https://github.com/metagn/grab") 22 | else: 23 | echo "docs task not implemented, need nimbleutils" 24 | 25 | task tests, "run tests for multiple backends": 26 | when declared(runTests): 27 | runTests(backends = {c, nims}) 28 | else: 29 | echo "tests task not implemented, need nimbleutils" 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grab 2 | 3 | Adds a `grab` statement for installing and importing Nimble packages 4 | directly through Nim code, similar to Groovy's Grape and `@Grab`. Works 5 | with NimScript, as all the computation is done at compile time. 6 | 7 | This installs the package globally, and can affect compilation time. For 8 | this reason it should generally only be used for scripts, tests, snippets and 9 | the like. 10 | 11 | ```nim 12 | import grab 13 | 14 | # install the package `regex` if not installed already, and import it 15 | grab "regex" 16 | 17 | assert "abc.123".match(re"\w+\.\d+") 18 | 19 | # run install command with the given arguments 20 | grab package("-y https://github.com/arnetheduck/nim-result@#HEAD", 21 | name = "result", forceInstall = true): # clarify package name to correctly query path 22 | # imports from the package directory 23 | import results 24 | 25 | func works(): Result[int, string] = 26 | result.ok(123) 27 | 28 | func fails(): Result[int, string] = 29 | result.err("abc") 30 | 31 | assert works().isOk 32 | assert fails().error == "abc" 33 | ``` 34 | 35 | Install with: 36 | 37 | ``` 38 | nimble install grab 39 | ``` 40 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: grab 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | workflow_dispatch: 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: jiro4989/setup-nim-action@v1 18 | 19 | - name: install nimbleutils 20 | run: nimble install -y https://github.com/metagn/nimbleutils@#HEAD 21 | 22 | - name: install dependencies 23 | run: nimble install -y 24 | 25 | - name: run tests 26 | run: nimble tests 27 | 28 | docs: 29 | runs-on: ubuntu-latest 30 | 31 | steps: 32 | - uses: actions/checkout@v3 33 | - uses: jiro4989/setup-nim-action@v1 34 | 35 | - name: install nimbleutils 36 | run: nimble install -y https://github.com/metagn/nimbleutils@#HEAD 37 | 38 | - name: install dependencies 39 | run: nimble install -y 40 | 41 | - name: compile docs 42 | run: nimble docs 43 | 44 | - name: move to subfolder 45 | run: | 46 | mkdir pages 47 | mv docs pages/ 48 | cd pages 49 | git init 50 | git add -A 51 | git config --local user.email "action@github.com" 52 | git config --local user.name "GitHub Action" 53 | git commit -m 'deploy' 54 | 55 | - name: push to branch 56 | uses: ad-m/github-push-action@master 57 | with: 58 | github_token: ${{ secrets.GITHUB_TOKEN }} 59 | branch: gh-pages 60 | force: true 61 | directory: ./pages/ 62 | if: github.event_name != 'pull_request' 63 | -------------------------------------------------------------------------------- /src/grab.nim: -------------------------------------------------------------------------------- 1 | ## Adds a `grab` statement for installing and importing Nimble packages 2 | ## directly through code. 3 | ## 4 | ## Works with NimScript. 5 | ## 6 | ## .. code-block:: nim 7 | ## import grab 8 | ## 9 | ## grab "regex" 10 | ## 11 | ## assert "abc.123".match(re"\w+\.\d+") 12 | ## 13 | ## grab package("-Y https://github.com/arnetheduck/nim-result@#HEAD", 14 | ## name = "result", forceInstall = true): 15 | ## import results 16 | ## 17 | ## func works(): Result[int, string] = 18 | ## result.ok(123) 19 | ## 20 | ## func fails(): Result[int, string] = 21 | ## result.err("abc") 22 | ## 23 | ## assert works().isOk 24 | ## assert fails().error == "abc" 25 | 26 | import macros, strutils, os 27 | 28 | when not compiles (var x = ""; x.delete(0 .. 0)): 29 | template delete(x: untyped, s: HSlice): untyped = 30 | let y = s 31 | x.delete(y.a, y.b) 32 | 33 | proc stripLeft(package: var string) = 34 | package.delete(0 .. max(max(package.rfind('/'), package.rfind('\\')), package.rfind(' '))) 35 | 36 | proc stripRight(package: var string) = 37 | template minim(a, b) = 38 | let c = b 39 | if a < 0 or c < a: a = c 40 | var len = package.find('?') 41 | len.minim(package.find('@')) 42 | if len < 0: len = package.len 43 | package.setLen(len) 44 | 45 | proc parseName(package: string): string = 46 | result = package 47 | stripLeft(result) 48 | stripRight(result) 49 | 50 | proc extractWithVersion(package: string): string = 51 | result = package 52 | stripLeft(result) 53 | var f = result.find('@') 54 | if f < 0: f = result.len 55 | var s = result.find('?') 56 | if s < 0: s = result.len + 1 57 | if s <= f: result.delete(s .. f) 58 | 59 | type Package* = object 60 | ## Package information to be used when installing and importing packages. 61 | name*, installCommand*, pathQuery*: string 62 | forceInstall*: bool 63 | 64 | proc package*(installCommand, name, pathQuery: string, forceInstall = false): Package = 65 | ## Generates package information with arguments to a `nimble install` 66 | ## command, package name, and optionally a name and version pair 67 | ## for the purpose of querying the module path. 68 | Package(installCommand: installCommand, 69 | name: parseName(name), 70 | pathQuery: pathQuery, 71 | forceInstall: forceInstall) 72 | 73 | proc package*(installCommand, name: string, forceInstall = false): Package = 74 | ## Generates package information with arguments to a `nimble install` 75 | ## command and a package name (optionally with a version). 76 | Package(installCommand: installCommand, 77 | name: parseName(name), 78 | pathQuery: name, 79 | forceInstall: forceInstall) 80 | 81 | proc package*(installCommand: string, forceInstall = false): Package = 82 | ## Converts the arguments of a `nimble install` command into 83 | ## package information. 84 | ## 85 | ## If the name of the package is different from the one assumed from 86 | ## the install command, then the package cannot be imported. In this case, 87 | ## a name or name and version pair must be given, such as 88 | ## ``package("fakename", "realname@0.1.0")``. 89 | Package(installCommand: installCommand, 90 | pathQuery: extractWithVersion(installCommand), 91 | name: parseName(installCommand), 92 | forceInstall: forceInstall) 93 | 94 | proc getPath(package: Package): string = 95 | for line in staticExec("nimble path " & package.pathQuery).splitLines: 96 | if line.len != 0: 97 | result = line 98 | 99 | proc grabImpl(package: Package, imports: NimNode): NimNode = 100 | when defined(grabGiveHint): 101 | hint("grabbing: " & $package, imports) 102 | 103 | let doPath = package.pathQuery.len != 0 104 | let doInstall = package.forceInstall or 105 | (doPath and not dirExists(getPath(package))) 106 | 107 | if doInstall: 108 | let installOutput = staticExec("nimble install -Y " & 109 | package.installCommand) 110 | if "Error: " in installOutput: 111 | error("could not install " & package.name & ", install log:\p" & 112 | installOutput, imports) 113 | 114 | let imports = 115 | if imports.len != 0: 116 | imports 117 | else: 118 | let x = ident(package.name) 119 | x.copyLineInfo(imports) 120 | newStmtList(newTree(nnkImportStmt, x)) 121 | 122 | let path = if doPath: getPath(package) else: "" 123 | if doPath and not dirExists(path): 124 | error("could not locate " & package.pathQuery & ", got error or invalid path:\p" & 125 | path, imports) 126 | 127 | proc patchImport(p: string, n: NimNode): NimNode = 128 | var root = n 129 | const replaceKinds = {nnkStrLit..nnkTripleStrLit, nnkIdent, nnkSym, nnkAccQuoted} 130 | proc replace(s: NimNode): NimNode = 131 | if p.len != 0: 132 | var str = (p / $s) 133 | if not str.endsWith(".nim"): 134 | str.add(".nim") 135 | newLit(str) 136 | else: 137 | s 138 | if root.kind in replaceKinds: 139 | replace root 140 | else: 141 | while root.len != 0: 142 | let index = if root.kind in {nnkCommand..nnkPostfix}: 1 else: 0 143 | if root[index].kind in replaceKinds: 144 | root[index] = replace root[index] 145 | break 146 | else: 147 | root = root[index] 148 | n 149 | 150 | result = copy imports 151 | for imp in result: 152 | case imp.kind 153 | of nnkImportStmt: 154 | for i in 0 ..< imp.len: 155 | imp[i] = patchImport(path, imp[i]) 156 | of nnkImportExceptStmt, nnkFromStmt, nnkIncludeStmt: 157 | imp[0] = patchImport(path, imp[0]) 158 | else: discard 159 | 160 | macro grab*(package: static Package, imports: untyped) = 161 | ## Installs a package with Nimble and immediately imports it. 162 | ## 163 | ## Can be followed with a list of imports from the package in an indented 164 | ## block. Imports outside this block will not work. By default, only 165 | ## the main module of the package is imported. 166 | ## 167 | ## This installs the package globally, and can fairly affect compilation time. 168 | ## For this reason it should only be used for scripts and snippets and the like. 169 | ## 170 | ## If the package is already installed, it will not reinstall it. 171 | ## This can be overriden by adding `-Y` at the start of the install command. 172 | ## 173 | ## See module documentation for usage. 174 | result = grabImpl(package, imports) 175 | 176 | macro grab*(installCommand: static string, imports: untyped) = 177 | ## Shorthand for `grab(package(installCommand), imports)`. 178 | ## 179 | ## See module documentation for usage. 180 | result = grabImpl(package(installCommand), imports) 181 | 182 | macro grab*(package) = 183 | ## Calls `grab(package, imports)` with the main module 184 | ## deduced from the package name imported by default. 185 | let imports = newNilLit() 186 | imports.copyLineInfo(package) 187 | let grabCall = ident("grab") 188 | grabCall.copyLineInfo(package) 189 | result = newCall(grabCall, package, imports) 190 | result.copyLineInfo(package) 191 | --------------------------------------------------------------------------------