├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── ci-bootstrap.cfg ├── tests ├── sample.cfg ├── nim.cfg ├── tspec.nim └── test.nim ├── .editorconfig ├── ci-docs.cfg ├── .gitignore ├── bootstrap.ps1 ├── bootstrap.sh ├── src ├── nimph.nim.cfg └── nimph │ ├── runner.nim │ ├── asjson.nim │ ├── nimble.nim │ ├── versiontags.nim │ ├── group.nim │ ├── package.nim │ ├── spec.nim │ ├── locker.nim │ ├── requirement.nim │ ├── version.nim │ ├── thehub.nim │ ├── doctor.nim │ ├── config.nim │ └── dependency.nim ├── LICENSE ├── choosenim ├── nimph.nimble ├── bootstrap-nonimble.sh ├── nimph.json └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: disruptek 2 | -------------------------------------------------------------------------------- /ci-bootstrap.cfg: -------------------------------------------------------------------------------- 1 | --hint[Link]=off 2 | --hint[Processing]=off 3 | --hint[Cc]=off 4 | --path="$nim" 5 | --path="$config" 6 | -------------------------------------------------------------------------------- /tests/sample.cfg: -------------------------------------------------------------------------------- 1 | --nimblePath="deps/pkgs" 2 | --path="src/nimph" 3 | -define:test2=foo 4 | -d:test3:foo 5 | -d:test4 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | insert_final_newline = true 4 | indent_size = 2 5 | trim_trailing_whitespace = true 6 | -------------------------------------------------------------------------------- /ci-docs.cfg: -------------------------------------------------------------------------------- 1 | --hint[Link]=off 2 | --hint[Processing]=off 3 | --hint[Cc]=off 4 | --clearNimblePath 5 | --nimblePath="$config/deps/pkgs" 6 | --path="$nim" 7 | --path="$config" 8 | -------------------------------------------------------------------------------- /tests/nim.cfg: -------------------------------------------------------------------------------- 1 | --define:ssl 2 | --path="../src" 3 | #--d:npegGraph 4 | #--d:npegTrace 5 | 6 | # specify our preferred version of libgit2 7 | --define:git2SetVer:"master" 8 | #--define:git2SetVer:"v0.28.3" 9 | 10 | # and our preferred method of retrieval 11 | --define:git2Git 12 | 13 | hint[XDeclaredButNotUsed]=off 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | nimblemeta.json 2 | nimbledeps 3 | deps 4 | bin 5 | tests/tconfig 6 | tests/tnimble 7 | tests/tpackage 8 | tests/tspec 9 | tests/tgit 10 | tests/ttags 11 | nim.cfg 12 | nimph.exe 13 | libcurl.so* 14 | libgit2.so* 15 | libmbedcrypto.so* 16 | libmbedtls.so* 17 | libmbedx509.so* 18 | libnghttp2.so* 19 | libssh2.so* 20 | libz.so* 21 | -------------------------------------------------------------------------------- /bootstrap.ps1: -------------------------------------------------------------------------------- 1 | if ( !(Join-Path 'src' 'nimph.nim' | Test-Path) ) { 2 | git clone git://github.com/disruptek/nimph.git 3 | Set-Location nimph 4 | } 5 | 6 | $env:NIMBLE_DIR = Join-Path $PWD 'deps' 7 | New-Item -Type Directory $env:NIMBLE_DIR -Force | Out-Null 8 | 9 | nimble --accept refresh 10 | nimble install "--passNim:--path:$(Resolve-Path 'src') --outDir:$PWD" 11 | -------------------------------------------------------------------------------- /bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if ! test -f src/nimph.nim; then 4 | git clone --depth 1 git://github.com/disruptek/nimph.git 5 | cd nimph 6 | fi 7 | 8 | export NIMBLE_DIR="`pwd`/nimbledeps" 9 | mkdir "$NIMBLE_DIR" 10 | 11 | nimble --accept refresh 12 | nimble --accept install unicodedb@0.7.2 nimterop@0.6.13 13 | nimble --accept install "--passNim:--path:\"`pwd`/src\" --outdir:\"`pwd`\"" 14 | 15 | if test -x nimph; then 16 | echo "nimph built successfully" 17 | else 18 | echo "unable to build nimph" 19 | exit 1 20 | fi 21 | -------------------------------------------------------------------------------- /src/nimph.nim.cfg: -------------------------------------------------------------------------------- 1 | # toggle these if you like 2 | #--define:cutelogEmojis 3 | #--define:cutelogMonochrome 4 | #--define:cutelogBland 5 | 6 | # not recommended 7 | #--define:gitErrorsAreFatal 8 | 9 | # try it out; it's horrible 10 | #--define:writeNimbleDirPaths=true 11 | 12 | # github won't work without ssl enabled 13 | --define:ssl 14 | 15 | # specify our preferred version of libgit2 16 | --define:git2SetVer:"v1.1.1" 17 | 18 | # and our preferred method of retrieval 19 | #--define:git2JBB 20 | --define:git2Git 21 | 22 | # remove dependency on rando nim cache 23 | #--define:git2Static 24 | 25 | --hint[Processing]=off 26 | --hint[Link]=off 27 | #--define:npegTrace 28 | 29 | # for gratuitous search path debugging 30 | #--define:debugPath 31 | 32 | # fix nimble? 33 | --path="$config" 34 | --path="$nim" 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Andy Davidoff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /choosenim: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | CHOOSE=`realpath $0` # make note of our origin 3 | NIM=`nim --hint[Conf]:off --dump.format:json dump config | jq -r .prefixdir`/.. 4 | if [ "$NIM" = "null/.." ]; then # true when the prefixdir is missing 5 | NIM=`dirname \`which nim\``/../.. # fallback for 1.0 support; see #127 6 | fi 7 | if [ $! -eq 0 ]; then # if nim threw an error due to a bad arg, 8 | exit 1 # fail so the user can deal with it 9 | fi 10 | cd "$NIM" 11 | if [ -n "$*" ]; then # a toolchain was requested 12 | if [ -d "$*" ]; then # the toolchain is available 13 | rm -f chosen # ffs my ln -sf should remove it 14 | ln -sf "$*" chosen # select the chosen toolchain 15 | if ! [ -f "chosen/bin/$CHOOSE" ]; then 16 | cp -p "$CHOOSE" chosen/bin # install choosenim if necessary 17 | fi 18 | nim --version # emit current toolchain version 19 | exit 0 # successful selection of toolchain 20 | fi 21 | fi 22 | tree -v -d -L 1 --noreport # report on available toolchains 23 | exit 1 # signify failure to switch 24 | -------------------------------------------------------------------------------- /nimph.nimble: -------------------------------------------------------------------------------- 1 | version = "1.0.10" 2 | author = "disruptek" 3 | description = "nim package handler from the future" 4 | license = "MIT" 5 | 6 | bin = @["nimph"] 7 | srcDir = "src" 8 | 9 | # this breaks tests 10 | #installDirs = @["docs", "tests", "src"] 11 | 12 | requires "https://github.com/c-blake/cligen >= 0.9.46 & < 2.0.0" 13 | requires "https://github.com/zevv/npeg >= 0.21.3 & < 1.0.0" 14 | requires "https://github.com/disruptek/bump >= 1.8.18 & < 2.0.0" 15 | requires "https://github.com/disruptek/github >= 2.0.3 & < 3.0.0" 16 | requires "https://github.com/disruptek/jsonconvert < 2.0.0" 17 | requires "https://github.com/disruptek/badresults >= 2.1.2 & < 3.0.0" 18 | requires "https://github.com/disruptek/cutelog >= 1.1.0 & < 2.0.0" 19 | requires "https://github.com/disruptek/gittyup >= 2.7.0 & < 3.0.0" 20 | requires "https://github.com/disruptek/ups >= 0.0.5 & < 1.0.0" 21 | 22 | when not defined(release): 23 | requires "https://github.com/disruptek/balls >= 3.0.0 & < 4.0.0" 24 | 25 | task test, "run unit tests": 26 | when defined(windows): 27 | exec """balls.cmd --define:git2Git --define:git2SetVer="v1.1.1" --define:ssl""" 28 | exec """balls.cmd --define:git2Git --define:git2SetVer="v1.1.1" --define:ssl --define:git2Static""" 29 | else: 30 | exec """balls --define:git2Git --define:git2SetVer="v1.1.1" --define:ssl""" 31 | exec """balls --define:git2Git --define:git2SetVer="v1.1.1" --define:ssl --define:git2Static""" 32 | -------------------------------------------------------------------------------- /bootstrap-nonimble.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | RELEASE="release" 4 | if test "$*" = "test"; then 5 | # reduce nimterop spam? 6 | RELEASE="release" 7 | fi 8 | 9 | cd src 10 | 11 | git clone --depth 1 https://github.com/disruptek/bump.git 12 | git clone --depth 1 --branch 1.1.2 https://github.com/disruptek/cutelog.git 13 | git clone --depth 1 https://github.com/disruptek/gittyup.git 14 | git clone --depth 1 https://github.com/disruptek/nimgit2.git 15 | git clone --depth 1 --branch v0.6.13 https://github.com/genotrance/nimterop.git 16 | git clone --depth 1 https://github.com/nitely/nim-regex.git 17 | git clone --depth 1 https://github.com/nitely/nim-unicodedb.git 18 | git clone --depth 1 https://github.com/nitely/nim-unicodeplus.git 19 | git clone --depth 1 https://github.com/nitely/nim-segmentation.git 20 | git clone --depth 1 https://github.com/c-blake/cligen.git 21 | git clone --depth 1 https://github.com/zevv/npeg.git 22 | git clone --depth 1 https://github.com/disruptek/jsonconvert.git 23 | git clone --depth 1 https://github.com/disruptek/badresults.git 24 | git clone --depth 1 https://github.com/disruptek/github.git 25 | git clone --depth 1 https://github.com/disruptek/rest.git 26 | git clone --depth 1 https://github.com/disruptek/foreach.git 27 | git clone --depth 1 https://github.com/disruptek/ups.git 28 | git clone --depth 1 https://github.com/disruptek/grok.git 29 | nim c --outdir=nimterop/nimterop --define:release --path:nim-regex/src --path:nim-unicodedb/src --path:nim-unicodeplus/src --path:nim-segmentation/src --path:cligen nimterop/nimterop/toast.nim 30 | nim c --outdir=nimterop/nimterop --define:release --path:nim-regex/src --path:nim-unicodedb/src --path:nim-unicodeplus/src --path:nim-segmentation/src --path:cligen nimterop/nimterop/loaf.nim 31 | nim c --outdir:.. --define:$RELEASE --path:ups --path:cligen --path:foreach --path:github/src --path:rest --path:npeg/src --path:jsonconvert --path:badresults --path:bump --path:cutelog --path:gittyup --path:nimgit2 --path:nimterop --path:nim-regex/src --path:nim-unicodedb/src --path:nim-unicodeplus/src --path:nim-segmentation/src --path:grok nimph.nim 32 | cd .. 33 | 34 | if test -x nimph; then 35 | echo "nimph built successfully" 36 | else 37 | echo "unable to build nimph" 38 | exit 1 39 | fi 40 | -------------------------------------------------------------------------------- /tests/tspec.nim: -------------------------------------------------------------------------------- 1 | import std/os 2 | import std/strutils 3 | import std/options 4 | import std/uri 5 | 6 | import pkg/bump 7 | import pkg/balls 8 | 9 | import nimph/spec 10 | import nimph/version 11 | 12 | suite "welcome to the nimph-o-matic 9000": 13 | proc v(loose: string): Version = 14 | let release = parseVersionLoosely(loose) 15 | result = release.get.version 16 | 17 | test "some url munging": 18 | let 19 | sshUrl = parseUri"git@github.com:disruptek/nimph.git" 20 | gitUrl = parseUri"git://github.com/disruptek/nimph.git" 21 | webUrl = parseUri"https://github.com/disruptek/nimph" 22 | bigUrl = parseUri"https://github.com/Vindaar/ginger" 23 | bagUrl = parseUri"https://githob.com/Vindaar/ginger" 24 | check "convert to git": 25 | $sshUrl.convertToGit == $gitUrl 26 | $gitUrl.convertToGit == $gitUrl 27 | $webUrl.convertToGit == $webUrl & ".git" # !!! 28 | #check "convert to ssh": 29 | checkpoint $sshUrl.convertToSsh 30 | checkpoint $gitUrl.convertToSsh 31 | checkpoint $webUrl.convertToSsh 32 | check $sshUrl.convertToSsh == $sshUrl 33 | check $gitUrl.convertToSsh == $sshUrl 34 | check $webUrl.convertToSsh == $sshUrl 35 | check "normalize path case (only) for github": 36 | $bigUrl.normalizeUrl == ($bigUrl).toLowerAscii 37 | $bagUrl.normalizeUrl == $bagUrl 38 | check $gitUrl.prepareForClone == $webUrl & ".git" # !!! 39 | 40 | test "fork targets": 41 | for url in [ 42 | parseUri"git@github.com:disruptek/nimph.git", 43 | parseUri"git://github.com/disruptek/nimph.git", 44 | parseUri"https://github.com/disruptek/nimph", 45 | ].items: 46 | let fork {.used.} = url.forkTarget 47 | checkpoint $url 48 | #checkpoint fork.repr 49 | check fork.ok 50 | check fork.owner == "disruptek" and fork.repo == "nimph" 51 | 52 | test "url normalization": 53 | let 54 | sshUser = "git" 55 | sshUrl1 = "git@git.sr.ht:~kungtotte/dtt" 56 | sshHost1 = "git.sr.ht" 57 | sshPath1 = "~kungtotte/dtt" 58 | sshUrl2 = "git@github.com:disruptek/nimph.git" 59 | sshHost2 = "github.com" 60 | sshPath2 = "disruptek/nimph.git" 61 | normUrl1 = normalizeUrl(parseUri(sshUrl1)) 62 | normUrl2 = normalizeUrl(parseUri(sshUrl2)) 63 | 64 | check "more creepy urls": 65 | normUrl1.username == sshUser 66 | normUrl1.hostname == sshHost1 67 | normUrl1.path == sshPath1 68 | normUrl2.username == sshUser 69 | normUrl2.hostname == sshHost2 70 | normUrl2.path == sshPath2 71 | 72 | test "path joins": 73 | let 74 | p = "goats" 75 | o = "pigs/" 76 | check "slash attack": 77 | ///p == "goats/" 78 | ///o == "pigs/" 79 | //////p == "/goats/" 80 | //////o == "/pigs/" 81 | -------------------------------------------------------------------------------- /src/nimph/runner.nim: -------------------------------------------------------------------------------- 1 | import std/strutils 2 | import std/strformat 3 | import std/logging 4 | import std/os 5 | import std/sequtils 6 | import std/osproc 7 | 8 | import nimph/spec 9 | 10 | type 11 | RunOutput* = object 12 | arguments*: seq[string] 13 | output*: string 14 | ok*: bool 15 | 16 | proc stripPkgs*(nimbleDir: string): string = 17 | ## omit and trailing /PkgDir from a path 18 | result = ///nimbleDir 19 | # the only way this is a problem is if the user stores deps in pkgs/pkgs, 20 | # but we can remove this hack once we have nimblePaths in nim-1.0 ... 21 | if result.endsWith(//////PkgDir): 22 | result = ///parentDir(result) 23 | 24 | proc runSomething*(exe: string; args: seq[string]; options: set[ProcessOption]; 25 | nimbleDir = ""): RunOutput = 26 | ## run a program with arguments, perhaps with a particular nimbleDir 27 | var 28 | command = findExe(exe) 29 | arguments = args 30 | opts = options 31 | block ran: 32 | if command == "": 33 | result = RunOutput(output: &"unable to find {exe} in path") 34 | warn result.output 35 | break ran 36 | 37 | if exe == "nimble": 38 | when defined(debug): 39 | arguments = @["--verbose"].concat arguments 40 | when defined(debugNimble): 41 | arguments = @["--debug"].concat arguments 42 | 43 | if nimbleDir != "": 44 | # we want to strip any trailing PkgDir arriving from elsewhere... 45 | var nimbleDir = nimbleDir.stripPkgs 46 | if not nimbleDir.dirExists: 47 | let emsg = &"{nimbleDir} is missing; can't run {exe}" # noqa 48 | raise newException(IOError, emsg) 49 | # the ol' belt-and-suspenders approach to specifying nimbleDir 50 | if exe == "nimble": 51 | arguments = @["--nimbleDir=" & nimbleDir].concat arguments 52 | putEnv("NIMBLE_DIR", nimbleDir) 53 | 54 | if poParentStreams in opts or poInteractive in opts: 55 | # sorry; i just find this easier to read than union() 56 | opts.incl poInteractive 57 | opts.incl poParentStreams 58 | # the user wants interactivity 59 | when defined(debug): 60 | debug command, arguments.join(" ") 61 | let 62 | process = startProcess(command, args = arguments, options = opts) 63 | result = RunOutput(ok: process.waitForExit == 0) 64 | else: 65 | # the user wants to capture output 66 | command &= " " & quoteShellCommand(arguments) 67 | when defined(debug): 68 | debug command 69 | let 70 | (output, code) = execCmdEx(command, opts) 71 | result = RunOutput(output: output, ok: code == 0) 72 | 73 | # for utility, also return the arguments we used 74 | result.arguments = arguments 75 | 76 | # a failure is worth noticing 77 | if not result.ok: 78 | notice exe & " " & arguments.join(" ") 79 | when defined(debug): 80 | debug "done running" 81 | -------------------------------------------------------------------------------- /src/nimph/asjson.nim: -------------------------------------------------------------------------------- 1 | import std/uri 2 | import std/strutils 3 | import std/strformat 4 | import std/options 5 | import std/json 6 | 7 | import bump 8 | 9 | import nimph/spec 10 | import nimph/version 11 | import nimph/package 12 | import nimph/requirement 13 | 14 | proc toJson*(operator: Operator): JsonNode = 15 | result = newJString($operator) 16 | 17 | proc toOperator*(js: JsonNode): Operator = 18 | result = parseEnum[Operator](js.getStr) 19 | 20 | proc toJson*(version: Version): JsonNode = 21 | result = newJArray() 22 | for index in VersionIndex.low .. VersionIndex.high: 23 | result.add newJInt(version.at(index).int) 24 | 25 | proc toVersion*(js: JsonNode): Version = 26 | let 27 | e = js.getElems 28 | if e.len != VersionIndex.high + 1: 29 | let emsg = &"dunno what to do with a version of len {e.len}" 30 | raise newException(ValueError, emsg) 31 | result = (major: e[0].getInt.uint, 32 | minor: e[1].getInt.uint, 33 | patch: e[2].getInt.uint) 34 | 35 | proc toJson*(mask: VersionMask): JsonNode = 36 | # is it a *.*.*? 37 | if mask.at(0).isNone: 38 | result = newJString("*") 39 | else: 40 | result = newJArray() 41 | for index in VersionIndex.low .. VersionIndex.high: 42 | let value = mask.at(index) 43 | if value.isSome: 44 | result.add newJInt(value.get.int) 45 | 46 | proc toVersionMask*(js: JsonNode): VersionMask = 47 | block: 48 | if js.kind == JString: 49 | # it's a *.*.* 50 | break 51 | 52 | # it's an array with items in it 53 | let 54 | e = js.getElems 55 | if e.high > VersionIndex.high: 56 | let emsg = &"dunno what to do with a version mask of len {e.len}" 57 | raise newException(ValueError, emsg) 58 | for index in VersionIndex.low .. VersionIndex.high: 59 | if index > e.high: 60 | break 61 | result[index] = e[index].getInt.uint.some 62 | 63 | proc toJson*(release: Release): JsonNode = 64 | result = newJObject() 65 | result["operator"] = release.kind.toJson 66 | case release.kind: 67 | of Tag: 68 | result["reference"] = newJString(release.reference) 69 | of Wild, Caret, Tilde: 70 | result["accepts"] = release.accepts.toJson 71 | of Equal, AtLeast, Over, Under, NotMore: 72 | result["version"] = release.version.toJson 73 | 74 | proc toRelease*(js: JsonNode): Release = 75 | result = Release(kind: js["operator"].toOperator) 76 | case result.kind: 77 | of Tag: 78 | result.reference = js["reference"].getStr 79 | of Wild, Caret, Tilde: 80 | result.accepts = js["accepts"].toVersionMask 81 | of Equal, AtLeast, Over, Under, NotMore: 82 | result.version = js["version"].toVersion 83 | 84 | proc toJson*(requirement: Requirement): JsonNode = 85 | result = newJObject() 86 | result["identity"] = newJString(requirement.identity) 87 | result["operator"] = requirement.operator.toJson 88 | result["release"] = requirement.release.toJson 89 | 90 | proc toRequirement*(js: JsonNode): Requirement = 91 | result = newRequirement(js["identity"].getStr, 92 | operator = js["operator"].toOperator, 93 | release = js["release"].toRelease) 94 | 95 | proc toJson*(dist: DistMethod): JsonNode = 96 | result = newJString($dist) 97 | 98 | proc toDistMethod*(js: JsonNode): DistMethod = 99 | result = parseEnum[DistMethod](js.getStr) 100 | 101 | proc toJson*(uri: Uri): JsonNode = 102 | let url = case uri.scheme: 103 | of "ssh", "": 104 | uri.convertToSsh 105 | else: 106 | uri.normalizeUrl 107 | result = newJString($url) 108 | 109 | proc toUri*(js: JsonNode): Uri = 110 | result = parseUri(js.getStr) 111 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | schedule: 4 | - cron: '30 5 * * *' 5 | 6 | push: 7 | branches: 8 | - master 9 | paths: 10 | - '**.cfg' 11 | - '**.nims' 12 | - '**.nim' 13 | - '**.nimble' 14 | - '**.sh' 15 | - 'tests/**' 16 | - '.github/workflows/ci.yml' 17 | 18 | pull_request: 19 | branches: 20 | - '*' 21 | paths: 22 | - '**.cfg' 23 | - '**.nims' 24 | - '**.nim' 25 | - '**.nimble' 26 | - '**.sh' 27 | - 'tests/**' 28 | - '.github/workflows/ci.yml' 29 | 30 | jobs: 31 | build: 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | #os: ['windows-latest', 'macos-latest', 'ubuntu-latest'] 36 | os: ['macos-latest', 'ubuntu-latest'] 37 | nim: ['devel', 'version-1-6', 'version-1-4', 'version-1-2'] 38 | name: '${{ matrix.os }} (${{ matrix.nim }})' 39 | runs-on: ${{ matrix.os }} 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v2 43 | with: 44 | path: ci 45 | 46 | - name: Setup Nim 47 | uses: alaviss/setup-nim@0.1.1 48 | with: 49 | path: nim 50 | version: ${{ matrix.nim }} 51 | 52 | - name: Run tests 53 | shell: bash 54 | run: | 55 | mkdir $HOME/.nimble 56 | cd ci 57 | git fetch --unshallow 58 | cp ci-bootstrap.cfg nim.cfg 59 | ./bootstrap-nonimble.sh test 60 | ./nimph refresh 61 | ./nimph 62 | ./nimph doctor || true 63 | cat nim.cfg 64 | rm nimph 65 | cp ci-bootstrap.cfg nim.cfg 66 | export NIMBLE_DIR=`pwd`/deps 67 | echo "--clearNimblePath" >> nim.cfg 68 | echo "--nimblePath=\"$NIMBLE_DIR/pkgs\"" >> nim.cfg 69 | cat nim.cfg 70 | ./bootstrap.sh 71 | ./nimph 72 | ./nimph doctor || true 73 | cat nim.cfg 74 | cd `./nimph path balls` 75 | nim c --out:$HOME/balls --define:release balls.nim 76 | cd - 77 | pushd deps/pkgs/nimterop-* 78 | nim c nimterop/toast.nim 79 | nim c nimterop/loaf.nim || true 80 | popd 81 | echo "remove nim's config.nims...?" 82 | ls -l `dirname \`which nim\``/../config/ 83 | rm `dirname \`which nim\``/../config/config.nims || true 84 | nim c -r --define:git2Git --define:git2SetVer="v1.1.1" --define:ssl tests/test.nim 85 | #nim c -r --define:git2Git --define:git2SetVer="v1.1.1" --define:ssl --define:git2Static tests/test.nim 86 | 87 | - name: Build docs 88 | if: ${{ matrix.docs }} == 'true' 89 | shell: bash 90 | run: | 91 | cd ci 92 | branch=${{ github.ref }} 93 | branch=${branch##*/} 94 | mv ci-docs.cfg nim.cfg 95 | rm -rf deps 96 | mkdir deps 97 | ./nimph doctor || true 98 | cat nim.cfg 99 | pushd deps/pkgs/nimterop-* 100 | nim c nimterop/toast.nim 101 | nim c nimterop/loaf.nim 102 | popd 103 | nim doc --project --outdir:docs \ 104 | '--git.url:https://github.com/${{ github.repository }}' \ 105 | '--git.commit:${{ github.sha }}' \ 106 | "--git.devel:$branch" \ 107 | src/nimph.nim 108 | # Ignore failures for older Nim 109 | cp docs/{the,}index.html || true 110 | 111 | - name: Publish docs 112 | if: > 113 | github.event_name == 'push' && github.ref == 'refs/heads/master' && 114 | matrix.os == 'ubuntu-latest' && matrix.nim == 'devel' 115 | uses: crazy-max/ghaction-github-pages@v1 116 | with: 117 | build_dir: ci/docs 118 | env: 119 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 120 | -------------------------------------------------------------------------------- /src/nimph/nimble.nim: -------------------------------------------------------------------------------- 1 | import std/uri 2 | import std/json 3 | import std/options 4 | import std/strtabs 5 | import std/strutils 6 | import std/os 7 | import std/osproc 8 | import std/strformat 9 | 10 | import npeg 11 | 12 | import nimph/spec 13 | import nimph/runner 14 | 15 | type 16 | DumpResult* = object 17 | table*: StringTableRef 18 | why*: string 19 | ok*: bool 20 | 21 | NimbleMeta* = ref object 22 | js: JsonNode 23 | link: seq[string] 24 | 25 | proc parseNimbleDump*(input: string): Option[StringTableRef] = 26 | ## parse output from `nimble dump` 27 | var 28 | table = newStringTable(modeStyleInsensitive) 29 | let 30 | peggy = peg "document": 31 | nl <- ?'\r' * '\n' 32 | white <- {'\t', ' '} 33 | key <- +(1 - ':') 34 | value <- '"' * *(1 - '"') * '"' 35 | errline <- white * >*(1 - nl) * +nl: 36 | warn $1 37 | line <- >key * ':' * +white * >value * +nl: 38 | table[$1] = unescape($2) 39 | anyline <- line | errline 40 | document <- +anyline * !1 41 | parsed = peggy.match(input) 42 | if parsed.ok: 43 | result = table.some 44 | 45 | proc fetchNimbleDump*(path: string; nimbleDir = ""): DumpResult = 46 | ## parse nimble dump output into a string table 47 | result = DumpResult(ok: false) 48 | block fetched: 49 | withinDirectory(path): 50 | let 51 | nimble = runSomething("nimble", 52 | @["dump", path], {poDaemon}, nimbleDir = nimbleDir) 53 | if not nimble.ok: 54 | result.why = "nimble execution failed" 55 | if nimble.output.len > 0: 56 | error nimble.output 57 | break fetched 58 | 59 | let 60 | parsed = parseNimbleDump(nimble.output) 61 | if parsed.isNone: 62 | result.why = &"unable to parse `nimble dump` output" 63 | break fetched 64 | result.table = parsed.get 65 | result.ok = true 66 | 67 | proc hasUrl*(meta: NimbleMeta): bool = 68 | ## true if the metadata includes a url 69 | result = "url" in meta.js 70 | result = result and meta.js["url"].kind == JString 71 | result = result and meta.js["url"].getStr != "" 72 | 73 | proc url*(meta: NimbleMeta): Uri = 74 | ## return the url associated with the package 75 | if not meta.hasUrl: 76 | raise newException(ValueError, "url not available") 77 | result = parseUri(meta.js["url"].getStr) 78 | if result.anchor == "": 79 | if "vcsRevision" in meta.js: 80 | result.anchor = meta.js["vcsRevision"].getStr 81 | removePrefix(result.anchor, {'#'}) 82 | 83 | proc writeNimbleMeta*(path: string; url: Uri; revision: string): bool = 84 | ## try to write a new nimblemeta.json 85 | block complete: 86 | if not dirExists(path): 87 | warn &"{path} is not a directory; cannot write {nimbleMeta}" 88 | break complete 89 | var 90 | revision = revision 91 | removePrefix(revision, {'#'}) 92 | var 93 | js = %* { 94 | "url": $url, 95 | "vcsRevision": revision, 96 | "files": @[], 97 | "binaries": @[], 98 | "isLink": false, 99 | } 100 | writer = open(path / nimbleMeta, fmWrite) 101 | defer: 102 | writer.close 103 | writer.write($js) 104 | result = true 105 | 106 | proc isLink*(meta: NimbleMeta): bool = 107 | ## true if the metadata says it's a link 108 | if meta.js.kind == JObject: 109 | result = meta.js.getOrDefault("isLink").getBool 110 | 111 | proc isValid*(meta: NimbleMeta): bool = 112 | ## true if the metadata appears to hold some data 113 | result = meta.js != nil and meta.js.len > 0 114 | 115 | proc fetchNimbleMeta*(path: string): NimbleMeta = 116 | ## parse the nimblemeta.json file if it exists 117 | result = NimbleMeta(js: newJObject()) 118 | let 119 | metafn = path / nimbleMeta 120 | try: 121 | if metafn.fileExists: 122 | let 123 | content = readFile(metafn) 124 | result.js = parseJson(content) 125 | except Exception as e: 126 | discard e # noqa 127 | warn &"error while trying to parse {nimbleMeta}: {e.msg}" 128 | -------------------------------------------------------------------------------- /src/nimph/versiontags.nim: -------------------------------------------------------------------------------- 1 | import std/strutils 2 | import std/sets 3 | import std/options 4 | import std/hashes 5 | import std/strtabs 6 | import std/tables 7 | 8 | import bump 9 | import gittyup 10 | 11 | import nimph/spec 12 | import nimph/version 13 | 14 | import nimph/group 15 | export group 16 | 17 | type 18 | VersionTags* = Group[Version, GitThing] 19 | 20 | proc addName*(group: var VersionTags; mask: VersionMask; thing: GitThing) = 21 | ## add a versionmask to the group; note that this overwrites semvers 22 | for symbol in mask.semanticVersionStrings: 23 | group.imports[symbol] = $thing.oid 24 | 25 | proc addName*(group: var VersionTags; version: Version; thing: GitThing) = 26 | ## add a version to the group; note that this overwrites semvers 27 | for symbol in version.semanticVersionStrings: 28 | group.imports[symbol] = $thing.oid 29 | 30 | proc add*(group: var VersionTags; ver: Version; thing: GitThing) = 31 | ## add a version to the group; note that this overwrites semvers 32 | group.table.add ver, thing 33 | group.addName ver, thing 34 | 35 | proc del*(group: var VersionTags; ver: Version) = 36 | ## remove a version from the group; note that this doesn't rebind semvers 37 | if group.table.hasKey(ver): 38 | group.delName $group.table[ver].oid 39 | group.table.del ver 40 | 41 | proc `[]=`*(group: var VersionTags; ver: Version; thing: GitThing) = 42 | ## set a key to a single value 43 | group.del ver 44 | group.add ver, thing 45 | 46 | proc `[]`*(group: VersionTags; ver: Version): var GitThing = 47 | ## get a git thing by version 48 | result = group.table[ver] 49 | 50 | proc `[]`*(group: VersionTags; ver: VersionMask): var GitThing = 51 | ## get a git thing by versionmask 52 | for symbol in ver.semanticVersionStrings: 53 | if group.imports.hasKey(symbol): 54 | let 55 | complete = group.imports[symbol] 56 | result = group.table[parseDottedVersion(complete)] 57 | break 58 | 59 | proc newVersionTags*(flags = defaultFlags): VersionTags = 60 | result = VersionTags(flags: flags) 61 | result.init(flags, mode = modeStyleInsensitive) 62 | 63 | iterator richen*(tags: GitTagTable): tuple[release: Release; thing: GitThing] = 64 | ## yield releases that match the tags and the things they represent 65 | if tags == nil: 66 | raise newException(Defect, "are you lost?") 67 | # we're yielding #someoid, #tag, and whatever we can parse (version, mask) 68 | for tag, thing in tags.pairs: 69 | # someoid 70 | yield (release: newRelease($thing.oid, operator = Tag), thing: thing) 71 | # tag 72 | yield (release: newRelease(tag, operator = Tag), thing: thing) 73 | let parsed = parseVersionLoosely(tag) 74 | if parsed.isSome: 75 | # 3.1.4 or 3.1.* 76 | yield (release: parsed.get, thing: thing) 77 | 78 | proc releaseHashes*(release: Release; head = ""): HashSet[Hash] = 79 | ## a set of hashes that should match valid values for the release 80 | result.incl hash(release) 81 | case release.kind: 82 | of Tag: 83 | # someNiceTag 84 | result.incl hash(release.reference) 85 | # perform the #head->oid substitution here 86 | if release.reference.toLowerAscii == "head" and head != "": 87 | result.incl head.hash 88 | result.incl "head".hash 89 | result.incl "HEAD".hash 90 | of Wildlings: 91 | # 3, 3.1, 3.1.4 ... as available 92 | let effective = release.accepts.effectively 93 | for semantic in effective.semanticVersionStrings: 94 | result.incl hash(semantic) 95 | for semantic in release.accepts.semanticVersionStrings: 96 | result.incl hash(semantic) 97 | result.incl hash(effective) 98 | result.incl hash($effective) 99 | else: 100 | # 3, 3.1, 3.1.4 ... as available 101 | for semantic in release.version.semanticVersionStrings: 102 | result.incl hash(semantic) 103 | result.incl hash(release.version) 104 | result.incl hash($release.version) 105 | 106 | proc releaseHashes*(release: Release; thing: GitThing; head = ""): HashSet[Hash] = 107 | ## a set of hashes that should match valid values for the release; 108 | ## the thing is presumed to be an associated tag/commit/etc and we 109 | ## should include useful hashes for it 110 | result = release.releaseHashes(head = head) 111 | # when we have a commit, we'll add the hash of the commit and its oid string 112 | result.incl hash(thing) 113 | result.incl hash($thing.oid) 114 | 115 | iterator matches*(tags: GitTagTable; against: HashSet[Hash]; 116 | head: string = ""): 117 | tuple[release: Release; thing: GitThing] = 118 | ## see if any of the releases in the tag table will match `against` 119 | ## if so, yield the release and thing 120 | if tags == nil: 121 | raise newException(Defect, "are you lost?") 122 | for release, thing in tags.richen: 123 | # compute hashes to match against 124 | var symbols = release.releaseHashes(thing, head = head) 125 | # see if we scored any matches 126 | if against.intersection(symbols).len != 0: 127 | yield (release: release, thing: thing) 128 | -------------------------------------------------------------------------------- /src/nimph/group.nim: -------------------------------------------------------------------------------- 1 | import std/os 2 | import std/strtabs 3 | import std/tables 4 | from std/sequtils import toSeq 5 | import std/uri except Url 6 | 7 | export strtabs.StringTableMode 8 | 9 | import nimph/spec 10 | 11 | type 12 | Group*[K; V: ref object] = ref object of RootObj 13 | table*: OrderedTableRef[K, V] 14 | imports*: StringTableRef 15 | flags*: set[Flag] 16 | mode: StringTableMode 17 | 18 | proc init*[K, V](group: Group[K, V]; flags: set[Flag]; mode = modeStyleInsensitive) = 19 | ## initialize the table and name cache 20 | group.table = newOrderedTable[K, V]() 21 | when K is Uri: 22 | group.mode = modeCaseSensitive 23 | else: 24 | group.mode = mode 25 | group.imports = newStringTable(group.mode) 26 | group.flags = flags 27 | 28 | proc addName[K: string, V](group: Group[K, V]; name: K; value: string) = 29 | ## add a name to the group, which points to value 30 | assert group.table.hasKey(value) 31 | group.imports[name] = value 32 | 33 | proc addName[K: Uri, V](group: Group[K, V]; url: K) = 34 | ## add a url to the group, which points to value 35 | assert group.table.hasKey(url) 36 | group.imports[$url] = $url 37 | when defined(debug): 38 | assert $url.bare notin group.imports 39 | group.imports[$url.bare] = $url 40 | 41 | proc delName*(group: Group; key: string) = 42 | ## remove a name from the group 43 | var 44 | remove: seq[string] 45 | # don't trust anyone; if the value matches, pull the name 46 | for name, value in group.imports.pairs: 47 | if value == key: 48 | remove.add name 49 | for name in remove: 50 | group.imports.del name 51 | 52 | proc del*[K: string, V](group: Group[K, V]; name: K) = 53 | ## remove from the group the named key and its associated value 54 | group.table.del name 55 | group.delName name 56 | 57 | proc del*[K: Uri, V](group: Group[K, V]; url: K) = 58 | ## remove from the group the url key and its associated value 59 | group.table.del url 60 | group.delName $url 61 | 62 | {.warning: "nim bug #12818".} 63 | proc len*[K, V](group: Group[K, V]): int = 64 | ## number of elements in the group 65 | result = group.table.len 66 | 67 | proc len*(group: Group): int = 68 | ## number of elements in the group 69 | result = group.table.len 70 | 71 | proc get*[K: string, V](group: Group[K, V]; key: K): V = 72 | ## fetch a value from the group using style-insensitive lookup 73 | if group.table.hasKey(key): 74 | result = group.table[key] 75 | elif group.imports.hasKey(key.importName): 76 | result = group.table[group.imports[key.importName]] 77 | else: 78 | let emsg = &"{key.importName} not found" 79 | raise newException(KeyError, emsg) 80 | 81 | proc mget*[K: string, V](group: var Group[K, V]; key: K): var V = 82 | ## fetch a value from the group using style-insensitive lookup 83 | if group.table.hasKey(key): 84 | result = group.table[key] 85 | elif group.imports.hasKey(key.importName): 86 | result = group.table[group.imports[key.importName]] 87 | else: 88 | let emsg = &"{key.importName} not found" 89 | raise newException(KeyError, emsg) 90 | 91 | proc `[]`*[K, V](group: var Group[K, V]; key: K): var V = 92 | ## fetch a value from the group using style-insensitive lookup 93 | result = group.mget(key) 94 | 95 | proc `[]`*[K, V](group: Group[K, V]; key: K): V = 96 | ## fetch a value from the group using style-insensitive lookup 97 | result = group.get(key) 98 | 99 | proc add*[K: string, V](group: Group[K, V]; key: K; value: V) = 100 | ## add a key and value to the group 101 | group.table.add key, value 102 | group.addName(key.importName, key) 103 | 104 | proc add*[K: string, V](group: Group[K, V]; url: Uri; value: V) = 105 | ## add a (bare) url as a key 106 | let 107 | naked = url.bare 108 | key = $naked 109 | group.table.add key, value 110 | # this gets picked up during instant-instantiation of a package from 111 | # a project's url, a la asPackage(project: Project): Package ... 112 | group.addName naked.importName, key 113 | 114 | proc `[]=`*[K, V](group: Group[K, V]; key: K; value: V) = 115 | ## set a key to a single value 116 | if group.hasKey(key): 117 | group.del key 118 | group.add key, value 119 | 120 | {.warning: "nim bug #12818".} 121 | proc add*[K: Uri, V](group: Group[K, V]; url: Uri; value: V) = 122 | ## add a (full) url as a key 123 | group.table.add url, value 124 | group.addName url 125 | 126 | iterator pairs*[K, V](group: Group[K, V]): tuple[key: K; val: V] = 127 | ## standard key/value pairs iterator 128 | for key, value in group.table.pairs: 129 | yield (key: key, val: value) 130 | 131 | {.warning: "nim bug #13510".} 132 | #iterator mpairs*[K, V](group: var Group[K, V]): tuple[key: K; val: var V] = 133 | iterator mpairs*[K, V](group: Group[K, V]): tuple[key: K; val: var V] = 134 | for key, value in group.table.mpairs: 135 | #yield (key: key, val: value) 136 | yield (key, value) 137 | 138 | iterator values*[K, V](group: Group[K, V]): V = 139 | ## standard value iterator 140 | for value in group.table.values: 141 | yield value 142 | 143 | iterator keys*[K, V](group: Group[K, V]): K = 144 | ## standard key iterator 145 | for key in group.table.keys: 146 | yield key 147 | 148 | iterator mvalues*[K, V](group: var Group[K, V]): var V = 149 | ## standard mutable value iterator 150 | for value in group.table.mvalues: 151 | yield value 152 | 153 | proc hasKey*[K, V](group: Group[K, V]; key: K): bool = 154 | ## true if the group contains the given key 155 | result = group.table.hasKey(key) 156 | 157 | proc contains*[K, V](group: Group[K, V]; key: K): bool = 158 | ## true if the group contains the given key or its importName 159 | result = group.table.contains(key) or group.imports.contains(key.importName) 160 | 161 | proc contains*[K, V](group: Group[K, V]; url: Uri): bool = 162 | ## true if a member of the group has the same (bare) url 163 | for value in group.values: 164 | if bareUrlsAreEqual(value.url, url): 165 | result = true 166 | break 167 | 168 | proc contains*[K, V](group: Group[K, V]; value: V): bool = 169 | ## true if the group contains the given value 170 | for v in group.values: 171 | if v == value: 172 | result = true 173 | break 174 | 175 | iterator reversed*[K, V](group: Group[K, V]): V = 176 | ## yield values in reverse order of entry 177 | let 178 | elems = toSeq group.values 179 | 180 | for index in countDown(elems.high, elems.low): 181 | yield elems[index] 182 | 183 | proc clear*[K, V](group: Group[K, V]) = 184 | ## clear the group without any other disruption 185 | group.table.clear 186 | group.imports.clear(group.mode) 187 | -------------------------------------------------------------------------------- /nimph.json: -------------------------------------------------------------------------------- 1 | { 2 | "lockfiles": { 3 | "demo of lockfiles": { 4 | "github": { 5 | "name": "github", 6 | "url": "https://github.com/disruptek/github", 7 | "release": { 8 | "operator": "#", 9 | "reference": "1.0.2" 10 | }, 11 | "requirement": { 12 | "identity": "github", 13 | "operator": "#", 14 | "release": { 15 | "operator": "#", 16 | "reference": "1.0.2" 17 | } 18 | }, 19 | "dist": "git" 20 | }, 21 | "npeg": { 22 | "name": "npeg", 23 | "url": "git://github.com/zevv/npeg.git", 24 | "release": { 25 | "operator": "#", 26 | "reference": "0.21.3" 27 | }, 28 | "requirement": { 29 | "identity": "npeg", 30 | "operator": "#", 31 | "release": { 32 | "operator": "#", 33 | "reference": "0.21.3" 34 | } 35 | }, 36 | "dist": "git" 37 | }, 38 | "rest": { 39 | "name": "rest", 40 | "url": "git://github.com/disruptek/rest.git", 41 | "release": { 42 | "operator": "#", 43 | "reference": "1.0.0" 44 | }, 45 | "requirement": { 46 | "identity": "https://github.com/disruptek/rest.git", 47 | "operator": "#", 48 | "release": { 49 | "operator": "#", 50 | "reference": "1.0.0" 51 | } 52 | }, 53 | "dist": "git" 54 | }, 55 | "foreach": { 56 | "name": "foreach", 57 | "url": "git@github.com:disruptek/foreach.git", 58 | "release": { 59 | "operator": "#", 60 | "reference": "1.0.2" 61 | }, 62 | "requirement": { 63 | "identity": "foreach", 64 | "operator": "#", 65 | "release": { 66 | "operator": "#", 67 | "reference": "1.0.2" 68 | } 69 | }, 70 | "dist": "git" 71 | }, 72 | "cligen": { 73 | "name": "cligen", 74 | "url": "https://github.com/c-blake/cligen.git", 75 | "release": { 76 | "operator": "#", 77 | "reference": "v0.9.41" 78 | }, 79 | "requirement": { 80 | "identity": "cligen", 81 | "operator": "#", 82 | "release": { 83 | "operator": "#", 84 | "reference": "v0.9.41" 85 | } 86 | }, 87 | "dist": "git" 88 | }, 89 | "bump": { 90 | "name": "bump", 91 | "url": "https://github.com/disruptek/bump", 92 | "release": { 93 | "operator": "#", 94 | "reference": "1.8.18" 95 | }, 96 | "requirement": { 97 | "identity": "bump", 98 | "operator": "#", 99 | "release": { 100 | "operator": "#", 101 | "reference": "1.8.18" 102 | } 103 | }, 104 | "dist": "git" 105 | }, 106 | "cutelog": { 107 | "name": "cutelog", 108 | "url": "git@github.com:disruptek/cutelog.git", 109 | "release": { 110 | "operator": "#", 111 | "reference": "1.1.1" 112 | }, 113 | "requirement": { 114 | "identity": "https://github.com/disruptek/cutelog", 115 | "operator": "#", 116 | "release": { 117 | "operator": "#", 118 | "reference": "1.1.1" 119 | } 120 | }, 121 | "dist": "git" 122 | }, 123 | "nimgit2": { 124 | "name": "nimgit2", 125 | "url": "https://github.com/genotrance/nimgit2.git", 126 | "release": { 127 | "operator": "#", 128 | "reference": "v0.1.1" 129 | }, 130 | "requirement": { 131 | "identity": "nimgit2", 132 | "operator": "#", 133 | "release": { 134 | "operator": "#", 135 | "reference": "v0.1.1" 136 | } 137 | }, 138 | "dist": "git" 139 | }, 140 | "nimterop": { 141 | "name": "nimterop", 142 | "url": "https://github.com/genotrance/nimterop.git", 143 | "release": { 144 | "operator": "#", 145 | "reference": "v0.3.6" 146 | }, 147 | "requirement": { 148 | "identity": "nimterop", 149 | "operator": "#", 150 | "release": { 151 | "operator": "#", 152 | "reference": "v0.3.6" 153 | } 154 | }, 155 | "dist": "git" 156 | }, 157 | "regex": { 158 | "name": "regex", 159 | "url": "https://github.com/nitely/nim-regex", 160 | "release": { 161 | "operator": "#", 162 | "reference": "v0.13.0" 163 | }, 164 | "requirement": { 165 | "identity": "regex", 166 | "operator": "#", 167 | "release": { 168 | "operator": "#", 169 | "reference": "v0.13.0" 170 | } 171 | }, 172 | "dist": "git" 173 | }, 174 | "unicodedb": { 175 | "name": "unicodedb", 176 | "url": "https://github.com/nitely/nim-unicodedb", 177 | "release": { 178 | "operator": "#", 179 | "reference": "v0.7.2" 180 | }, 181 | "requirement": { 182 | "identity": "unicodedb", 183 | "operator": "#", 184 | "release": { 185 | "operator": "#", 186 | "reference": "v0.7.2" 187 | } 188 | }, 189 | "dist": "git" 190 | }, 191 | "unicodeplus": { 192 | "name": "unicodeplus", 193 | "url": "https://github.com/nitely/nim-unicodeplus", 194 | "release": { 195 | "operator": "#", 196 | "reference": "v0.5.1" 197 | }, 198 | "requirement": { 199 | "identity": "unicodeplus", 200 | "operator": "#", 201 | "release": { 202 | "operator": "#", 203 | "reference": "v0.5.1" 204 | } 205 | }, 206 | "dist": "git" 207 | }, 208 | "unittest2": { 209 | "name": "unittest2", 210 | "url": "https://github.com/stefantalpalaru/nim-unittest2", 211 | "release": { 212 | "operator": "#", 213 | "reference": "30c7d332d8ebab28d3240018f48f145ff20af239" 214 | }, 215 | "requirement": { 216 | "identity": "https://github.com/stefantalpalaru/nim-unittest2", 217 | "operator": "#", 218 | "release": { 219 | "operator": "#", 220 | "reference": "30c7d332d8ebab28d3240018f48f145ff20af239" 221 | } 222 | }, 223 | "dist": "git" 224 | }, 225 | "": { 226 | "name": "", 227 | "url": "git@github.com:disruptek/nimph.git", 228 | "release": { 229 | "operator": "#", 230 | "reference": "ce2d0a8dbf129b05f681438e2d21f932142eb5e6" 231 | }, 232 | "requirement": { 233 | "identity": "nimph", 234 | "operator": "#", 235 | "release": { 236 | "operator": "#", 237 | "reference": "ce2d0a8dbf129b05f681438e2d21f932142eb5e6" 238 | } 239 | }, 240 | "dist": "git" 241 | } 242 | } 243 | } 244 | } -------------------------------------------------------------------------------- /src/nimph/package.nim: -------------------------------------------------------------------------------- 1 | import std/strtabs 2 | import std/tables 3 | import std/times 4 | import std/os 5 | import std/hashes 6 | import std/strformat 7 | import std/sequtils 8 | import std/strutils 9 | import std/uri 10 | import std/json 11 | import std/options 12 | 13 | import npeg 14 | 15 | import nimph/spec 16 | import nimph/requirement 17 | 18 | import nimph/group 19 | export group 20 | 21 | type 22 | DistMethod* = enum 23 | Local = "local" 24 | Git = "git" 25 | Nest = "nest" 26 | Merc = "hg" 27 | 28 | Package* = ref object 29 | name*: string 30 | url*: Uri 31 | dist*: DistMethod 32 | tags*: seq[string] 33 | description*: string 34 | license*: string 35 | web*: Uri 36 | naive*: bool 37 | local*: bool 38 | path*: string 39 | author*: string 40 | 41 | PackageGroup* = Group[string, Package] 42 | 43 | PackagesResult* = object 44 | ok*: bool 45 | why*: string 46 | packages*: PackageGroup 47 | info: FileInfo 48 | 49 | proc importName*(package: Package): string = 50 | result = package.name.importName.toLowerAscii 51 | error &"import name {result} from {package.name}" 52 | 53 | proc newPackage*(name: string; path: string; dist: DistMethod; 54 | url: Uri): Package = 55 | ## create a new package that probably points to a local repo 56 | result = Package(name: name, dist: dist, url: url, 57 | path: path, local: path.dirExists) 58 | 59 | proc newPackage*(name: string; dist: DistMethod; url: Uri): Package = 60 | ## create a new package 61 | result = Package(name: name, dist: dist, url: url) 62 | 63 | proc newPackage*(url: Uri): Package = 64 | ## create a new package with only a url 65 | result = newPackage(name = url.packageName, dist = Git, 66 | url = url.convertToGit) 67 | # flag this package as not necessarily named correctly; 68 | # we had to guess at what the final name might be... 69 | result.naive = true 70 | 71 | proc newPackage(name: string; license: string; description: string): Package = 72 | ## create a new package for nimble's package list consumer 73 | result = Package(name: name, license: license, description: description) 74 | 75 | proc `$`*(package: Package): string = 76 | result = package.name 77 | if package.naive: 78 | result &= " (???)" 79 | 80 | proc newPackageGroup*(flags: set[Flag] = defaultFlags): PackageGroup = 81 | ## instantiate a new package group for collecting a list of packages 82 | result = PackageGroup(flags: flags) 83 | result.init(flags, mode = modeStyleInsensitive) 84 | 85 | proc aimAt*(package: Package; req: Requirement): Package = 86 | ## produce a refined package which might meet the requirement 87 | var 88 | aim = package.url 89 | if aim.anchor == "": 90 | aim.anchor = req.release.asUrlAnchor 91 | 92 | result = newPackage(name = package.name, dist = package.dist, url = aim) 93 | result.license = package.license 94 | result.description = package.description 95 | result.tags = package.tags 96 | result.naive = false 97 | result.web = package.web 98 | 99 | proc add(group: PackageGroup; js: JsonNode) = 100 | ## how packages get added to a group from the json list 101 | var 102 | name = js["name"].getStr 103 | package = newPackage(name = name, 104 | license = js.getOrDefault("license").getStr, 105 | description = js.getOrDefault("description").getStr) 106 | 107 | if "alias" in js: 108 | raise newException(ValueError, "don't add aliases thusly") 109 | 110 | if "url" in js: 111 | package.url = js["url"].getStr.parseUri 112 | if "web" in js: 113 | package.web = js["web"].getStr.parseUri 114 | else: 115 | package.web = package.url 116 | if "method" in js: 117 | package.dist = parseEnum[DistMethod](js["method"].getStr) 118 | if "author" in js: 119 | package.author = js["author"].getStr 120 | else: 121 | package.dist = Git # let's be explicit here 122 | if "tags" in js: 123 | package.tags = mapIt(js["tags"], it.getStr.toLowerAscii) 124 | 125 | group.add name, package 126 | 127 | proc getOfficialPackages*(nimbleDir: string): PackagesResult {.raises: [].} = 128 | ## parse the official packages list from nimbledir 129 | var 130 | filename = ///nimbleDir 131 | if filename.endsWith(//////PkgDir): 132 | filename = nimbledir.parentDir / officialPackages 133 | else: 134 | filename = nimbledir / officialPackages 135 | 136 | # make sure we have a sane return value 137 | result = PackagesResult(ok: false, why: "", packages: newPackageGroup()) 138 | 139 | var group = result.packages 140 | block parsing: 141 | try: 142 | # we might not even have to open the file; wouldn't that be wonderful? 143 | if not nimbledir.dirExists or not filename.fileExists: 144 | result.why = &"{filename} not found" 145 | break 146 | 147 | # grab the file info for aging purposes 148 | result.info = getFileInfo(filename) 149 | 150 | # okay, i guess we have to read and parse this silly thing 151 | let 152 | content = readFile(filename) 153 | js = parseJson(content) 154 | 155 | # consume the json array 156 | var 157 | aliases: seq[tuple[name: string; alias: string]] 158 | for node in js.items: 159 | # if it's an alias, stash it for later 160 | if "alias" in node: 161 | aliases.add (node.getOrDefault("name").getStr, 162 | node["alias"].getStr) 163 | continue 164 | 165 | # else try to add it to the group 166 | try: 167 | group.add node 168 | except Exception as e: 169 | notice node 170 | warn &"error parsing package: {e.msg}" 171 | 172 | # now add in the aliases we collected 173 | for name, alias in aliases.items: 174 | if alias in group: 175 | group.add name, group.get(alias) 176 | else: 177 | warn &"alias `{name}` refers to a missing package `{alias}`" 178 | 179 | # add a style-insensitive alias for the opposite case package-name 180 | let 181 | keys = toSeq group.keys 182 | for key in keys.items: 183 | # key -> "Goats_And_Pigs" 184 | {.warning: "work-around for arc bug".} 185 | let package = group[key] 186 | group[key.toLowerAscii] = package 187 | group[key.toUpperAscii] = package 188 | 189 | result.ok = true 190 | except Exception as e: 191 | result.why = e.msg 192 | 193 | proc ageInDays*(found: PackagesResult): int64 = 194 | ## days since the packages file was last refreshed 195 | result = (getTime() - found.info.lastWriteTime).inDays 196 | 197 | proc toUrl*(requirement: Requirement; group: PackageGroup): Option[Uri] = 198 | ## try to determine the distribution url for a requirement 199 | var url: Uri 200 | 201 | # if it could be a url, try to parse it as such 202 | result = requirement.toUrl 203 | if result.isNone: 204 | # otherwise, see if we can find it in the package group 205 | if requirement.identity in group: 206 | let 207 | package = group.get(requirement.identity) 208 | if package.dist notin {Local, Git}: 209 | warn &"the `{package.dist}` distribution method is unsupported" 210 | else: 211 | url = package.url 212 | result = url.some 213 | debug "parsed in packages", requirement 214 | 215 | # maybe stuff the reference into the anchor 216 | if result.isSome: 217 | url = result.get 218 | url.anchor = requirement.release.asUrlAnchor 219 | result = url.some 220 | 221 | proc hasUrl*(group: PackageGroup; url: Uri): bool = 222 | ## true if the url seems to match a package in the group 223 | for value in group.values: 224 | result = bareUrlsAreEqual(value.url.convertToGit, 225 | url.convertToGit) 226 | if result: 227 | break 228 | 229 | proc matching*(group: PackageGroup; req: Requirement): PackageGroup = 230 | ## select a subgroup of packages that appear to match the requirement 231 | result = newPackageGroup() 232 | if req.isUrl: 233 | let 234 | findurl = req.toUrl(group) 235 | if findurl.isNone: 236 | let emsg = &"couldn't parse url for requirement {req}" # noqa 237 | raise newException(ValueError, emsg) 238 | for name, package in group.pairs: 239 | if bareUrlsAreEqual(package.url.convertToGit, 240 | findurl.get.convertToGit): 241 | result.add name, package.aimAt(req) 242 | when defined(debug): 243 | debug "matched the url in packages", $package.url 244 | else: 245 | for name, package in group.pairs: 246 | if name == req.identity: 247 | result.add name, package.aimAt(req) 248 | when defined(debug): 249 | debug "matched the package by name" 250 | 251 | iterator urls*(group: PackageGroup): Uri = 252 | ## yield (an ideally git) url for each package in the group 253 | for package in group.values: 254 | yield if package.dist == Git: 255 | package.url.convertToGit 256 | else: 257 | package.url 258 | -------------------------------------------------------------------------------- /src/nimph/spec.nim: -------------------------------------------------------------------------------- 1 | import std/strformat 2 | import std/options 3 | import std/strutils 4 | import std/hashes 5 | import std/uri 6 | import std/os 7 | import std/times 8 | 9 | import compiler/pathutils 10 | 11 | import cutelog 12 | export cutelog 13 | 14 | import ups/sanitize 15 | 16 | # slash attack /////////////////////////////////////////////////// 17 | when NimMajor >= 1 and NimMinor >= 1: 18 | template `///`*(a: string): string = 19 | # ensure a trailing DirSep 20 | joinPath(a, $DirSep, "") 21 | template `///`*(a: AbsoluteFile | AbsoluteDir): string = 22 | # ensure a trailing DirSep 23 | `///`(a.string) 24 | template `//////`*(a: string | AbsoluteFile | AbsoluteDir): string = 25 | # ensure a trailing DirSep and a leading DirSep 26 | joinPath($DirSep, "", `///`(a), $DirSep, "") 27 | else: 28 | template `///`*(a: string): string = 29 | # ensure a trailing DirSep 30 | joinPath(a, "") 31 | template `///`*(a: AbsoluteFile | AbsoluteDir): string = 32 | # ensure a trailing DirSep 33 | `///`(a.string) 34 | template `//////`*(a: string | AbsoluteFile | AbsoluteDir): string = 35 | # ensure a trailing DirSep and a leading DirSep 36 | "" / "" / `///`(a) / "" 37 | 38 | type 39 | Flag* {.pure.} = enum 40 | Quiet 41 | Strict 42 | Force 43 | Dry 44 | Safe 45 | Network 46 | 47 | RollGoal* = enum 48 | Upgrade = "upgrade" 49 | Downgrade = "downgrade" 50 | Specific = "roll" 51 | 52 | ForkTargetResult* = object 53 | ok*: bool 54 | why*: string 55 | owner*: string 56 | repo*: string 57 | url*: Uri 58 | 59 | const 60 | dotNimble* {.strdefine.} = "".addFileExt("nimble") 61 | dotNimbleLink* {.strdefine.} = "".addFileExt("nimble-link") 62 | dotGit* {.strdefine.} = "".addFileExt("git") 63 | dotHg* {.strdefine.} = "".addFileExt("hg") 64 | DepDir* {.strdefine.} = //////"deps" 65 | PkgDir* {.strdefine.} = //////"pkgs" 66 | NimCfg* {.strdefine.} = "nim".addFileExt("cfg") 67 | ghTokenFn* {.strdefine.} = "github_api_token" 68 | ghTokenEnv* {.strdefine.} = "NIMPH_TOKEN" 69 | hubTokenFn* {.strdefine.} = "".addFileExt("config") / "hub" 70 | stalePackages* {.intdefine.} = 14 71 | configFile* {.strdefine.} = "nimph".addFileExt("json") 72 | nimbleMeta* {.strdefine.} = "nimblemeta".addFileExt("json") 73 | officialPackages* {.strdefine.} = "packages_official".addFileExt("json") 74 | emptyRelease* {.strdefine.} = "#head" 75 | defaultRemote* {.strdefine.} = "origin" 76 | upstreamRemote* {.strdefine.} = "upstream" 77 | excludeMissingSearchPaths* {.booldefine.} = false 78 | excludeMissingLazyPaths* {.booldefine.} = true 79 | writeNimbleDirPaths* {.booldefine.} = false 80 | shortDate* = initTimeFormat "yyyy-MM-dd" 81 | # add Safe to defaultFlags to, uh, default to Safe mode 82 | defaultFlags*: set[Flag] = {Quiet, Strict} 83 | 84 | # when true, try to clamp analysis to project-local directories 85 | WhatHappensInVegas* = false 86 | 87 | template withinDirectory*(path: string; body: untyped): untyped = 88 | if not path.dirExists: 89 | raise newException(ValueError, path & " is not a directory") 90 | let cwd = getCurrentDir() 91 | setCurrentDir(path) 92 | defer: 93 | setCurrentDir(cwd) 94 | body 95 | 96 | template isValid*(url: Uri): bool = url.scheme.len != 0 97 | 98 | proc hash*(url: Uri): Hash = 99 | ## help hash URLs 100 | var h: Hash = 0 101 | for field in url.fields: 102 | when field is string: 103 | h = h !& field.hash 104 | elif field is bool: 105 | h = h !& field.hash 106 | result = !$h 107 | 108 | proc bare*(url: Uri): Uri = 109 | result = url 110 | result.anchor = "" 111 | 112 | proc bareUrlsAreEqual*(a, b: Uri): bool = 113 | ## compare two urls without regard to their anchors 114 | if a.isValid and b.isValid: 115 | var 116 | x = a.bare 117 | y = b.bare 118 | result = $x == $y 119 | 120 | proc pathToImport*(path: string): string = 121 | ## calculate how a path will be imported by the compiler 122 | assert path.len > 0 123 | result = path.lastPathPart.split("-")[0] 124 | assert result.len > 0 125 | 126 | proc normalizeUrl*(uri: Uri): Uri = 127 | result = uri 128 | if result.scheme == "" and result.path.contains("@"): 129 | let 130 | usersep = result.path.find("@") 131 | pathsep = result.path.find(":") 132 | result.path = uri.path[pathsep+1 .. ^1] 133 | result.username = uri.path[0 ..< usersep] 134 | result.hostname = uri.path[usersep+1 ..< pathsep] 135 | result.scheme = "ssh" 136 | 137 | # we used to do some ->git conversions here but they make increasingly 138 | # little sense since we really cannot be sure the user will be able to 139 | # use them, and with this doubt, we should err on the side of trusting 140 | # our input since it was, y'know, provided by a programmer. 141 | 142 | # https://github.com/disruptek/nimph/issues/145 143 | # we need to remove case-sensitivity of github paths 144 | if result.hostname.toLowerAscii == "github.com": 145 | result.path = result.path.toLowerAscii 146 | 147 | proc convertToGit*(uri: Uri): Uri = 148 | ## convert a url from any format (we will normalize it) 149 | ## into something like git://github.com/disruptek/nimph.git 150 | result = uri.normalizeUrl 151 | if result.scheme in ["", "http", "ssh"]: 152 | result.scheme = "git" 153 | if not result.path.endsWith(".git"): 154 | result.path &= ".git" 155 | result.username = "" 156 | 157 | proc convertToSsh*(uri: Uri): Uri = 158 | ## convert a url from any format (we will normalize it) 159 | ## into something like git@github.com:disruptek/nimph.git 160 | result = uri.convertToGit 161 | result.username = uri.username 162 | if not result.path[0].isAlphaNumeric: 163 | result.path = result.path[1..^1] 164 | if uri.username == "": 165 | result.username = "git" 166 | result.path = result.username & "@" & result.hostname & ":" & result.path 167 | result.username = "" 168 | result.hostname = "" 169 | result.scheme = "" 170 | 171 | proc prepareForClone*(uri: Uri): Uri = 172 | ## rewrite a url for the purposes of conducting a clone; 173 | ## this currently only has bearing on github urls, which 174 | ## must be rewritten to https if possible, since we cannot 175 | ## rely on the user's keys being correct 176 | result = normalizeUrl uri 177 | if result.hostname.toLowerAscii == "github.com": 178 | if result.scheme in ["ssh", "git", "http"]: 179 | result.scheme = "https" 180 | # add .git for consistency 181 | if not result.path.endsWith(".git"): 182 | result.path &= ".git" 183 | if result.username == "git": 184 | result.username = "" 185 | 186 | proc packageName*(name: string): string = 187 | ## return a string that is plausible as a package name 188 | when true: 189 | result = name 190 | else: 191 | const capsOkay = 192 | when FilesystemCaseSensitive: 193 | true 194 | else: 195 | false 196 | let 197 | sane = name.sanitizeIdentifier(capsOkay = capsOkay) 198 | if sane.isSome: 199 | result = sane.get 200 | else: 201 | raise newException(ValueError, "unable to sanitize `" & name & "`") 202 | 203 | proc packageName*(url: Uri): string = 204 | ## guess the name of a package from a url 205 | when defined(debug) or defined(debugPath): 206 | assert url.isValid 207 | var 208 | # ensure the path doesn't end in a slash 209 | path = url.path 210 | removeSuffix(path, {'/'}) 211 | result = packageName(path.extractFilename.changeFileExt("")) 212 | 213 | proc importName*(path: string): string = 214 | ## a uniform name usable in code for imports 215 | assert path.len > 0 216 | # strip any leading directories and extensions 217 | result = splitFile(path).name 218 | const capsOkay = 219 | when FilesystemCaseSensitive: 220 | true 221 | else: 222 | false 223 | let 224 | sane = path.sanitizeIdentifier(capsOkay = capsOkay) 225 | # if it's a sane identifier, use it 226 | if sane.isSome: 227 | result = $(get sane) 228 | elif not capsOkay: 229 | # emit a lowercase name on case-insensitive filesystems 230 | result = path.toLowerAscii 231 | # else, we're just emitting the existing file's basename 232 | 233 | proc importName*(url: Uri): string = 234 | let url = url.normalizeUrl 235 | if not url.isValid: 236 | raise newException(ValueError, "invalid url: " & $url) 237 | elif url.scheme == "file": 238 | result = url.path.importName 239 | else: 240 | result = url.packageName.importName 241 | 242 | proc forkTarget*(url: Uri): ForkTargetResult = 243 | result.url = url.normalizeUrl 244 | block success: 245 | if not result.url.isValid: 246 | result.why = &"url is invalid" 247 | break 248 | if result.url.hostname.toLowerAscii != "github.com": 249 | result.why = &"url {result.url} does not point to github" 250 | break 251 | if result.url.path.len < 1: 252 | result.why = &"unable to parse url {result.url}" 253 | break 254 | # split /foo/bar into (bar, foo) 255 | let start = if result.url.path.startsWith("/"): 1 else: 0 256 | (result.owner, result.repo) = result.url.path[start..^1].splitPath 257 | # strip .git 258 | if result.repo.endsWith(".git"): 259 | result.repo = result.repo[0..^len("git+2")] 260 | result.ok = result.owner.len > 0 and result.repo.len > 0 261 | if not result.ok: 262 | result.why = &"unable to parse url {result.url}" 263 | 264 | {.warning: "replace this with compiler code".} 265 | proc destylize*(s: string): string = 266 | ## this is how we create a uniformly comparable token 267 | result = s.toLowerAscii.replace("_") 268 | -------------------------------------------------------------------------------- /src/nimph/locker.nim: -------------------------------------------------------------------------------- 1 | import std/json 2 | import std/hashes 3 | import std/strformat 4 | import std/strtabs 5 | import std/tables 6 | import std/uri 7 | 8 | import nimph/spec 9 | import nimph/version 10 | import nimph/group 11 | import nimph/config 12 | import nimph/project 13 | import nimph/dependency 14 | import nimph/package 15 | import nimph/asjson 16 | import nimph/doctor 17 | import nimph/requirement 18 | 19 | type 20 | Locker* = ref object 21 | name*: string 22 | url*: Uri 23 | requirement*: Requirement 24 | dist*: DistMethod 25 | release*: Release 26 | LockerRoom* = ref object of Group[string, Locker] 27 | name*: string 28 | root*: Locker 29 | 30 | const 31 | # we use "" as a sigil to indicate the root of the project because 32 | # it's not a valid import name and won't be accepted by Group 33 | rootName = "" 34 | 35 | proc hash*(locker: Locker): Hash = 36 | # this is how we'll test equivalence 37 | var h: Hash = 0 38 | h = h !& locker.name.hash 39 | h = h !& locker.release.hash 40 | result = !$h 41 | 42 | proc hash*(room: LockerRoom): Hash = 43 | ## the hash of a lockerroom is the hash of its root and all lockers 44 | var h: Hash = 0 45 | for locker in room.values: 46 | h = h !& locker.hash 47 | h = h !& room.root.hash 48 | result = !$h 49 | 50 | proc `==`(a, b: Locker): bool = 51 | result = a.hash == b.hash 52 | 53 | proc `==`(a, b: LockerRoom): bool = 54 | result = a.hash == b.hash 55 | 56 | proc newLockerRoom*(name = ""; flags = defaultFlags): LockerRoom = 57 | result = LockerRoom(name: name, flags: flags) 58 | result.init(flags, mode = modeStyleInsensitive) 59 | 60 | proc newLocker(requirement: Requirement): Locker = 61 | result = Locker(requirement: requirement) 62 | 63 | proc newLocker(req: Requirement; name: string; project: Project): Locker = 64 | ## we use the req's identity and the project's release; this might need 65 | ## to change to simply use the project name, depending on an option... 66 | result = newRequirement(req.identity, Equal, project.release).newLocker 67 | result.url = project.url 68 | result.name = name 69 | result.dist = project.dist 70 | result.release = project.release 71 | 72 | proc newLockerRoom*(project: Project; flags = defaultFlags): LockerRoom = 73 | ## a new lockerroom using the project release as the root 74 | let 75 | requirement = newRequirement(project.name, Equal, project.release) 76 | result = newLockerRoom(flags = flags) 77 | result.root = newLocker(requirement, rootName, project) 78 | 79 | proc add*(room: var LockerRoom; req: Requirement; name: string; 80 | project: Project) = 81 | ## create a new locker for the requirement from the project and 82 | ## safely add it to the lockerroom 83 | var locker = newLocker(req, name, project) 84 | block found: 85 | for existing in room.values: 86 | if existing == locker: 87 | error &"unable to add equivalent lock for `{name}`" 88 | break found 89 | room.add name, locker 90 | 91 | proc fillRoom(room: var LockerRoom; dependencies: DependencyGroup): bool = 92 | ## fill a lockerroom with lockers constructed from the dependency tree; 93 | ## returns true if there were no missing/unready/shadowed dependencies 94 | result = true 95 | for requirement, dependency in dependencies.pairs: 96 | var shadowed = false 97 | if dependency.projects.len == 0: 98 | warn &"missing requirement {requirement}" 99 | result = false 100 | continue 101 | for project in dependency.projects.values: 102 | if not shadowed: 103 | shadowed = true 104 | if dependency.names.len > 1: 105 | warn &"multiple import names for {requirement}" 106 | for name in dependency.names.items: 107 | if project.dist != Git: 108 | warn &"{project} isn't in git; it's {project.dist} {project.repo}" 109 | elif not project.repoLockReady: 110 | result = false 111 | if room.hasKey(name): 112 | warn &"clashing import {name}" 113 | result = false 114 | continue 115 | room.add requirement, name, project 116 | continue 117 | warn &"shadowed project {project}" 118 | result = false 119 | 120 | proc fillDeps(dependencies: var DependencyGroup; 121 | room: LockerRoom; project: Project): bool = 122 | ## fill a dependency tree with lockers and run dependency resolution 123 | ## using the project; returns true if there were no resolution failures 124 | result = true 125 | for locker in room.values: 126 | var 127 | req = newRequirement(locker.requirement.identity, Equal, locker.release) 128 | dependency = req.newDependency 129 | discard dependencies.addedRequirements(dependency) 130 | result = result and project.resolve(dependencies, req) 131 | 132 | proc toJson*(locker: Locker): JsonNode = 133 | ## convert a Locker to a JObject 134 | result = newJObject() 135 | result["name"] = newJString(locker.name) 136 | result["url"] = locker.url.toJson 137 | result["release"] = locker.release.toJson 138 | result["requirement"] = locker.requirement.toJson 139 | result["dist"] = locker.dist.toJson 140 | 141 | proc toLocker*(js: JsonNode): Locker = 142 | ## convert a JObject to a Locker 143 | let 144 | req = js["requirement"].toRequirement 145 | result = req.newLocker 146 | result.name = js["name"].getStr 147 | result.url = js["url"].toUri 148 | result.release = js["release"].toRelease 149 | result.dist = js["dist"].toDistMethod 150 | 151 | proc toJson*(room: LockerRoom): JsonNode = 152 | ## convert a LockerRoom to a JObject 153 | result = newJObject() 154 | for name, locker in room.pairs: 155 | result[locker.name] = locker.toJson 156 | result[room.root.name] = room.root.toJson 157 | 158 | proc toLockerRoom*(js: JsonNode; name = ""): LockerRoom = 159 | ## convert a JObject to a LockerRoom 160 | result = newLockerRoom(name) 161 | for name, locker in js.pairs: 162 | if name == rootName: 163 | result.root = locker.toLocker 164 | elif result.hasKey(name): 165 | error &"ignoring duplicate locker `{name}`" 166 | else: 167 | result.add name, locker.toLocker 168 | 169 | proc getLockerRoom*(project: Project; name: string; room: var LockerRoom): bool = 170 | ## true if we pulled the named lockerroom out of the project's configuration 171 | let 172 | js = project.config.getLockerRoom(name) 173 | if js != nil and js.kind == JObject: 174 | room = js.toLockerRoom(name) 175 | result = true 176 | 177 | iterator allLockerRooms*(project: Project): LockerRoom = 178 | ## emit each lockerroom in the project's configuration 179 | for name, js in project.config.getAllLockerRooms.pairs: 180 | yield js.toLockerRoom(name) 181 | 182 | proc unlock*(project: var Project; name: string; flags = defaultFlags): bool = 183 | ## unlock a project using the named lockfile 184 | var 185 | dependencies = project.newDependencyGroup(flags = {Flag.Quiet} + flags) 186 | room = newLockerRoom(name, flags) 187 | 188 | block unlocked: 189 | if not project.getLockerRoom(name, room): 190 | notice &"unable to find a lock named `{name}`" 191 | break unlocked 192 | 193 | # warn about any locks performed against non-Git distributions 194 | for name, locker in room.pairs: 195 | if locker.dist != Git: 196 | let emsg = &"unsafe lock of `{name}` for " & 197 | &"{locker.requirement} as {locker.release}" # noqa 198 | warn emsg 199 | 200 | # perform doctor resolution of dependencies, etc. 201 | var 202 | state = DrState(kind: DrRetry) 203 | while state.kind == DrRetry: 204 | # it's our game to lose 205 | result = true 206 | # resolve dependencies for the lock 207 | if not dependencies.fillDeps(room, project): 208 | notice &"unable to resolve all dependencies for `{name}`" 209 | result = false 210 | state.kind = DrError 211 | # see if we can converge the environment to the lock 212 | elif not project.fixDependencies(dependencies, state): 213 | notice "failed to fix all dependencies" 214 | result = false 215 | # if the doctor doesn't want us to try again, we're done 216 | if state.kind notin {DrRetry}: 217 | break 218 | # empty the dependencies and rescan for projects 219 | dependencies.reset(project) 220 | 221 | proc lock*(project: var Project; name: string; flags = defaultFlags): bool = 222 | ## store a project's dependencies into the named lockfile 223 | var 224 | dependencies = project.newDependencyGroup(flags = {Flag.Quiet} + flags) 225 | room = newLockerRoom(project, flags) 226 | 227 | block locked: 228 | if project.getLockerRoom(name, room): 229 | notice &"lock `{name}` already exists; choose a new name" 230 | break locked 231 | 232 | # if we cannot resolve our dependencies, we can't lock the project 233 | result = project.resolve(dependencies) 234 | if not result: 235 | notice &"unable to resolve all dependencies for {project}" 236 | break locked 237 | 238 | # if the lockerroom isn't confident, we can't lock the project 239 | result = room.fillRoom(dependencies) 240 | if not result: 241 | notice &"not confident enough to lock {project}" 242 | break locked 243 | 244 | # compare this lockerroom to pre-existing lockerrooms and don't dupe it 245 | for exists in project.allLockerRooms: 246 | if exists == room: 247 | notice &"already locked these dependencies as `{exists.name}`" 248 | result = false 249 | break locked 250 | 251 | # write the lockerroom to the project's configuration 252 | project.config.addLockerRoom name, room.toJson 253 | -------------------------------------------------------------------------------- /tests/test.nim: -------------------------------------------------------------------------------- 1 | import std/strtabs 2 | import std/os 3 | import std/strutils 4 | import std/options 5 | import std/tables 6 | import std/uri 7 | 8 | import bump 9 | import gittyup 10 | import balls 11 | 12 | import nimph/spec 13 | import nimph/config 14 | import nimph/project 15 | import nimph/nimble 16 | import nimph/package 17 | import nimph/version 18 | import nimph/requirement 19 | import nimph/dependency 20 | import nimph/versiontags 21 | 22 | block: 23 | # let us shadow `project` 24 | 25 | suite "welcome to the nimph-o-matic 9000": 26 | const 27 | sample = "tests/sample.cfg" 28 | testcfg = newTarget(sample) 29 | was = staticRead(sample.extractFilename) 30 | 31 | var project: Project 32 | var deps: DependencyGroup 33 | 34 | test "open the project": 35 | let target = findTarget(".") 36 | check "finding targets": 37 | target.found.isSome 38 | findProject(project, (get target.found).repo) 39 | 40 | test "load a nim.cfg": 41 | let loaded = parseConfigFile(sample) 42 | check loaded.isSome 43 | 44 | test "naive parse": 45 | let parsed = parseProjectCfg(testcfg) 46 | check parsed.ok 47 | check "nimblePath" in parsed.table 48 | checkpoint $parsed.table 49 | check parsed.table["path"].len > 1 50 | for find in ["test4", "test3:foo", "test2=foo"]: 51 | block found: 52 | for value in parsed.table.values: 53 | if value == find: 54 | break found 55 | fail "missing config values from parse" 56 | 57 | test "add a line to a config": 58 | check testcfg.appendConfig("--clearNimblePath") 59 | let now = readFile(sample) 60 | check "splitlines": 61 | # check for empty trailing line 62 | was.splitLines.len + 2 == now.splitLines.len 63 | now.splitLines[^1] == "" 64 | writeFile(sample, was) 65 | 66 | test "parse some dump output": 67 | let text = """oneline: "is fine"""" & "\n" 68 | let parsed = parseNimbleDump(text) 69 | check parsed.isSome 70 | 71 | test "via subprocess capture": 72 | let dumped = fetchNimbleDump(project.nimble.repo) 73 | check dumped.ok == true 74 | if dumped.ok: 75 | check dumped.table["Name"] == "nimph" 76 | 77 | const 78 | # how we'll render a release requirement like "package" 79 | anyRelease = "*" 80 | 81 | test "parse simple requires statements": 82 | let 83 | text1 = "nim >= 0.18.0, bump 1.8.6, github < 2.0.0" 84 | text2 = "" 85 | text3 = "nim #catsAndDogsLivingTogether" 86 | text4 = "goats" 87 | text5 = "goats ^1.2.3" 88 | text6 = "nim#catsAndDogsLivingTogether" 89 | text7 = "pigs 2.*.*" 90 | text8 = "git://github.com/disruptek/bump.git#1.8.8" 91 | text9 = "git://github.com/disruptek/bump.git" 92 | text10 = "pigs 2.*" 93 | text11 = "dogs ^3.2" 94 | text12 = "owls ~4" 95 | text13 = "owls any version" 96 | text14 = "owls >=1.0.0 &< 2" 97 | parsed1 = parseRequires(text1) 98 | parsed2 = parseRequires(text2) 99 | parsed3 = parseRequires(text3) 100 | parsed4 = parseRequires(text4) 101 | parsed5 = parseRequires(text5) 102 | parsed6 = parseRequires(text6) 103 | parsed7 = parseRequires(text7) 104 | parsed8 = parseRequires(text8) 105 | parsed9 = parseRequires(text9) 106 | parsed10 = parseRequires(text10) 107 | parsed11 = parseRequires(text11) 108 | parsed12 = parseRequires(text12) 109 | parsed13 = parseRequires(text13) 110 | parsed14 = parseRequires(text14) 111 | check parsed1.isSome 112 | check parsed2.isSome 113 | check parsed3.isSome 114 | check parsed4.isSome 115 | for req in parsed4.get.values: 116 | check $req.release == anyRelease 117 | check parsed5.isSome 118 | check parsed6.isSome 119 | for req in parsed6.get.values: 120 | check req.release.reference == "catsAndDogsLivingTogether" 121 | check parsed7.isSome 122 | for req in parsed7.get.values: 123 | check $req.release == "2" 124 | check parsed8.isSome 125 | for req in parsed8.get.values: 126 | check req.identity == "git://github.com/disruptek/bump.git" 127 | check req.release.reference == "1.8.8" 128 | for req in parsed9.get.values: 129 | check req.identity == "git://github.com/disruptek/bump.git" 130 | check $req.release == anyRelease 131 | for req in parsed10.get.values: 132 | check req.identity == "pigs" 133 | for req in parsed11.get.values: 134 | check req.identity == "dogs" 135 | for req in parsed12.get.values: 136 | check req.identity == "owls" 137 | check not req.isSatisfiedBy newRelease"1.8.8" 138 | for req in parsed13.get.values: 139 | check $req.release == anyRelease 140 | check req.isSatisfiedBy newRelease"1.8.8" 141 | check parsed14.get.len == 2 142 | for req in parsed14.get.values: 143 | checkpoint $req 144 | 145 | test "parse nimph requires statement": 146 | project.fetchDump() 147 | let 148 | text = project.dump["requires"] 149 | parsed = parseRequires(text) 150 | check parsed.isSome 151 | 152 | test "naive package naming": 153 | check "nim_Somepack" == importName(parseUri"git@github.com:some/nim-Somepack.git/") 154 | check "nim_Somepack" == importName(parseUri"git@github.com:some/nim-Somepack.git") 155 | check "somepack" == importName("/some/other/somepack-1.2.3".pathToImport) 156 | 157 | test "get the official packages list": 158 | let 159 | parsed = getOfficialPackages(project.nimbleDir) 160 | check parsed.ok == true 161 | check "release" in parsed.packages["bump"].tags 162 | 163 | test "requirements versus versions": 164 | let 165 | works = [ 166 | newRequirement("a", Equal, "1.2.3"), 167 | newRequirement("a", AtLeast, "1.2.3"), 168 | newRequirement("a", NotMore, "1.2.3"), 169 | newRequirement("a", Caret, "1"), 170 | newRequirement("a", Caret, "1.2"), 171 | newRequirement("a", Caret, "1.2.3"), 172 | newRequirement("a", Tilde, "1"), 173 | newRequirement("a", Tilde, "1.2"), 174 | newRequirement("a", Tilde, "1.2.0"), 175 | ] 176 | breaks = [ 177 | newRequirement("a", Equal, "1.2.4"), 178 | newRequirement("a", AtLeast, "1.2.4"), 179 | newRequirement("a", NotMore, "1.2.2"), 180 | newRequirement("a", Caret, "2"), 181 | newRequirement("a", Caret, "1.3"), 182 | newRequirement("a", Caret, "1.2.4"), 183 | newRequirement("a", Tilde, "0"), 184 | newRequirement("a", Tilde, "1.1"), 185 | newRequirement("a", Tilde, "1.1.2"), 186 | ] 187 | one23 = newRelease("1.2.3") 188 | for req in works.items: 189 | check req.isSatisfiedBy one23 190 | for req in breaks.items: 191 | check not req.isSatisfiedBy one23 192 | 193 | test "parse version loosely": 194 | let 195 | works = [ 196 | "v1.2.3", 197 | "V. 1.2.3", 198 | "1.2.3-rc2", 199 | "1.2.3a", 200 | "1.2.3", 201 | "1.2.3.4", 202 | "mary had a little l1.2.3mb whose fleece... ah you get the picture" 203 | ] 204 | for v in works.items: 205 | let parsed = v.parseVersionLoosely 206 | check parsed.isSome 207 | check $parsed.get == "1.2.3" 208 | check "".parseVersionLoosely.isNone 209 | 210 | block: 211 | ## load project config 212 | project.cfg = loadAllCfgs project.repo 213 | 214 | block: 215 | ## dependencies, path-for-name, project-for-path 216 | deps = newDependencyGroup(project, {Dry}) 217 | check project.resolve(deps) 218 | var path = deps.pathForName "cutelog" 219 | check path.isSome 220 | check dirExists(get path) 221 | var proj = deps.projectForPath path.get 222 | check proj.isSome 223 | check (get proj).name == "cutelog" 224 | 225 | repository := openRepository project.gitDir: 226 | fail"unable to open the repo" 227 | 228 | test "roll between versions": 229 | returnToHeadAfter project: 230 | for ver in ["0.6.6", "0.6.5"]: 231 | let release = newRelease(ver, operator = Tag) 232 | let req = newRequirement($project.url, operator = Tag, release) 233 | if project.rollTowards(req): 234 | for stat in repository.status(ssIndexAndWorkdir): 235 | check stat.isOk 236 | check gsfIndexModified notin stat.get.flags 237 | 238 | test "project version changes": 239 | returnToHeadAfter project: 240 | let versioned = project.versionChangingCommits 241 | let required = project.requirementChangingCommits 242 | when false: 243 | for key, value in versioned.pairs: 244 | checkpoint "versioned ", key 245 | for key, value in required.pairs: 246 | checkpoint "required ", key 247 | check "version oids as expected": 248 | $versioned[v"0.6.5"].oid == "8937c0b998376944fd93d6d8e7b3cf4db91dfb9b" 249 | $versioned[v"0.6.6"].oid == "5a3de5a5fc9b83d5a9bba23f7e950b37a96d10e6" 250 | 251 | test "basic tag table fetch": 252 | fetchTagTable project 253 | check project.tags != nil, "tag fetch yielded no table" 254 | check project.tags.len > 0, "tag fetch created empty table" 255 | 256 | test "make sure richen finds a tag": 257 | check not project.tags.isNil, "tag fetch unsuccessful" 258 | block found: 259 | for release, thing in project.tags.richen: 260 | when false: 261 | checkpoint $release 262 | checkpoint $thing 263 | if release == newRelease("0.6.14", operator = Tag): 264 | break found 265 | fail"tag for 0.6.14 was not found" 266 | -------------------------------------------------------------------------------- /src/nimph/requirement.nim: -------------------------------------------------------------------------------- 1 | import std/options 2 | import std/strutils 3 | import std/strformat 4 | import std/tables 5 | import std/uri except Url 6 | import std/hashes 7 | 8 | import bump 9 | import npeg 10 | 11 | import nimph/spec 12 | import nimph/version 13 | 14 | type 15 | # the specification of a package requirement 16 | Requirement* = ref object 17 | identity*: string 18 | operator*: Operator 19 | release*: Release 20 | child*: Requirement 21 | notes*: string 22 | 23 | Requires* = OrderedTableRef[Requirement, Requirement] 24 | 25 | proc `$`*(req: Requirement): string = 26 | result = &"{req.identity}{req.operator}{req.release}" 27 | 28 | proc isValid*(req: Requirement): bool = 29 | ## true if the requirement seems sensible 30 | result = req.release.isValid 31 | if result: 32 | case req.operator: 33 | # if the operator is Tag, it's essentially a #== test 34 | of Tag: 35 | result = req.release.kind in {Tag} 36 | # if the operator supports a mask, then so might the release 37 | of Caret, Tilde, Wild: 38 | result = req.release.kind in {Wild, Equal} 39 | # if the operator supports only equality, apply it to tags, versions 40 | of Equal: 41 | result = req.release.kind in {Tag, Equal} 42 | # else it can only be a relative comparison to a complete version spec 43 | else: 44 | result = req.release.kind in {Equal} 45 | 46 | proc isSatisfiedBy(requirement: Requirement; version: Version): bool = 47 | ## true if the version satisfies the requirement 48 | let 49 | op = requirement.operator 50 | case op: 51 | of Tag: 52 | # try to parse a version from the tag and see if it matches exactly 53 | result = version == requirement.release.effectively 54 | of Caret: 55 | # the caret logic is designed to match that of cargo 56 | block caret: 57 | let accepts = requirement.release.accepts 58 | for index, field in accepts.pairs: 59 | if field.isNone: 60 | break 61 | if result == false: 62 | if field.get != 0: 63 | if field.get != version.at(index): 64 | result = false 65 | break caret 66 | result = true 67 | elif field.get > version.at(index): 68 | result = false 69 | break caret 70 | of Tilde: 71 | # the tilde logic is designed to match that of cargo 72 | block tilde: 73 | let accepts = requirement.release.accepts 74 | for index, field in accepts.pairs: 75 | if field.isNone or index == VersionIndex.high: 76 | break 77 | if field.get != version.at(index): 78 | result = false 79 | break tilde 80 | result = true 81 | of Wild: 82 | # wildcards match 3.1.* or simple strings like "3" (3.*.*) 83 | let accepts = requirement.release.accepts 84 | # all the fields must be acceptable 85 | if acceptable(accepts.major, op, version.major): 86 | if acceptable(accepts.minor, op, version.minor): 87 | if acceptable(accepts.patch, op, version.patch): 88 | result = true 89 | of Equal: 90 | result = version == requirement.release.version 91 | of AtLeast: 92 | result = version >= requirement.release.version 93 | of NotMore: 94 | result = version <= requirement.release.version 95 | of Under: 96 | result = version < requirement.release.version 97 | of Over: 98 | result = version > requirement.release.version 99 | 100 | proc isSatisfiedBy*(req: Requirement; spec: Release): bool = 101 | ## true if the requirement is satisfied by the specification 102 | case req.operator: 103 | of Tag: 104 | result = spec.reference == req.release.reference 105 | of Equal: 106 | result = spec == req.release 107 | of AtLeast: 108 | result = spec >= req.release 109 | of NotMore: 110 | result = spec <= req.release 111 | of Under: 112 | result = spec < req.release 113 | of Over: 114 | result = spec > req.release 115 | of Tilde, Caret, Wild: 116 | # check if the wildcard matches everything (only Wild, in theory) 117 | if req.release.accepts.major.isNone: 118 | result = true 119 | # otherwise, we might be able to treat it as a version 120 | elif spec.isSpecific: 121 | result = req.isSatisfiedBy spec.specifically 122 | # else we're gonna have to abstract "3" to "3.0.0" 123 | else: 124 | result = req.isSatisfiedBy spec.effectively 125 | 126 | proc hash*(req: Requirement): Hash = 127 | ## uniquely identify a requirement 128 | var h: Hash = 0 129 | h = h !& req.identity.hash 130 | h = h !& req.operator.hash 131 | h = h !& req.release.hash 132 | if req.child != nil: 133 | h = h !& req.child.hash 134 | result = !$h 135 | 136 | proc adopt*(parent: var Requirement; child: Requirement) = 137 | ## combine two requirements 138 | if parent != child: 139 | if parent.child == nil: 140 | parent.child = child 141 | else: 142 | parent.child.adopt child 143 | 144 | iterator children*(parent: Requirement; andParent = false): Requirement = 145 | ## yield the children of a parent requirement 146 | var req = parent 147 | if andParent: 148 | yield req 149 | while req.child != nil: 150 | req = req.child 151 | yield req 152 | 153 | proc newRequirement*(id: string; operator: Operator; 154 | release: Release, notes = ""): Requirement = 155 | ## create a requirement from a release, eg. that of a project 156 | when defined(debug): 157 | if id != id.strip: 158 | warn &"whitespace around requirement identity: `{id}`" 159 | if id == "": 160 | raise newException(ValueError, "requirements must have length, if not girth") 161 | result = Requirement(identity: id.strip, release: release, notes: notes) 162 | # if it parsed as Caret, Tilde, or Wild, then paint the requirement as such 163 | if result.release.kind in Wildlings: 164 | result.operator = result.release.kind 165 | elif result.release.kind in {Tag}: 166 | # eventually, we'll support tag comparisons... 167 | {.warning: "tag comparisons unsupported".} 168 | result.operator = result.release.kind 169 | else: 170 | result.operator = operator 171 | 172 | proc newRequirement*(id: string; operator: Operator; spec: string): Requirement = 173 | ## parse a requirement from a string 174 | result = newRequirement(id, operator, newRelease(spec, operator = operator)) 175 | 176 | proc newRequirement(id: string; operator: string; spec: string): Requirement = 177 | ## parse a requirement with the given operator from a string 178 | var 179 | op = Equal 180 | # using "" to mean "==" was retarded and i refuse to map my Equal 181 | # enum to "" in capitulation; nil carborundum illegitimi 182 | if operator != "": 183 | op = parseEnum[Operator](operator) 184 | result = newRequirement(id, op, spec) 185 | 186 | iterator orphans*(parent: Requirement): Requirement = 187 | ## yield each requirement without their kids 188 | for child in parent.children(andParent = true): 189 | yield newRequirement(id = child.identity, operator = child.operator, 190 | release = child.release, notes = child.notes) 191 | 192 | proc parseRequires*(input: string): Option[Requires] = 193 | ## parse a `requires` string output from `nimble dump` 194 | ## also supports `~` and `^` and `*` operators a la cargo 195 | var 196 | requires = Requires() 197 | lastname: string 198 | 199 | let 200 | peggy = peg "document": 201 | white <- {'\t', ' '} 202 | url <- +Alnum * "://" * +(1 - white - ending - '#') 203 | name <- url | +(Alnum | '_') 204 | ops <- ">=" | "<=" | ">" | "<" | "==" | "~" | "^" | 0 205 | dstar <- +Digit | '*' 206 | ver <- (dstar * ('.' * dstar)[0..2]) | "any version" 207 | ending <- (*white * "," * *white) | (*white * "&" * *white) | !1 208 | tag <- '#' * +(1 - ending) 209 | spec <- tag | ver 210 | anyrecord <- >name: 211 | lastname = $1 212 | let req = newRequirement(id = $1, operator = Wild, spec = "*") 213 | if req notin requires: 214 | requires[req] = req 215 | andrecord <- *white * >ops * *white * >spec: 216 | let req = newRequirement(id = lastname, operator = $1, spec = $2) 217 | if req notin requires: 218 | requires[req] = req 219 | inrecord <- >name * *white * >ops * *white * >spec: 220 | lastname = $1 221 | let req = newRequirement(id = $1, operator = $2, spec = $3) 222 | if req notin requires: 223 | requires[req] = req 224 | record <- (inrecord | andrecord | anyrecord) * ending 225 | document <- *record 226 | parsed = peggy.match(input) 227 | if parsed.ok: 228 | result = requires.some 229 | 230 | proc isVirtual*(requirement: Requirement): bool = 231 | ## is the requirement something we should overlook? 232 | result = requirement.identity.toLowerAscii in ["nim"] 233 | 234 | proc isUrl*(requirement: Requirement): bool = 235 | ## a terrible way to determine if the requirement is a url 236 | result = ':' in requirement.identity 237 | 238 | proc asUrlAnchor*(release: Release): string = 239 | ## produce a suitable url anchor referencing a release 240 | case release.kind: 241 | of Tag: 242 | result = release.reference 243 | of Equal: 244 | result = $release.version 245 | else: 246 | debug &"no url-as-anchor for {release.kind}" 247 | removePrefix(result, {'#'}) 248 | 249 | proc toUrl*(requirement: Requirement): Option[Uri] = 250 | ## try to determine the distribution url for a requirement 251 | # if it could be a url, try to parse it as such 252 | if requirement.isUrl: 253 | try: 254 | var url = parseUri(requirement.identity) 255 | if requirement.release.kind in {Equal, Tag}: 256 | url.anchor = requirement.release.asUrlAnchor 257 | result = url.some 258 | except: 259 | warn &"unable to parse requirement `{requirement.identity}`" 260 | 261 | proc importName*(requirement: Requirement): string = 262 | ## guess the import name given only a requirement 263 | block: 264 | if requirement.isUrl: 265 | let url = requirement.toUrl 266 | if url.isSome: 267 | result = url.get.importName 268 | break 269 | result = requirement.identity.importName 270 | 271 | proc describe*(requirement: Requirement): string = 272 | ## describe a requirement and where it may have come from, if possible 273 | result = $requirement 274 | if requirement.notes != "": 275 | result &= " from " & requirement.notes 276 | -------------------------------------------------------------------------------- /src/nimph/version.nim: -------------------------------------------------------------------------------- 1 | import std/strformat 2 | import std/hashes 3 | import std/strutils 4 | import std/tables 5 | import std/options 6 | 7 | import bump 8 | import npeg 9 | 10 | import nimph/spec 11 | 12 | type 13 | VersionField* = typeof(Version.major) 14 | VersionIndex* = range[0 .. 2] 15 | VersionMaskField* = Option[VersionField] 16 | VersionMask* = object 17 | major*: VersionMaskField 18 | minor*: VersionMaskField 19 | patch*: VersionMaskField 20 | 21 | Operator* = enum 22 | Tag = "#" 23 | Wild = "*" 24 | Tilde = "~" 25 | Caret = "^" 26 | Equal = "==" 27 | AtLeast = ">=" 28 | Over = ">" 29 | Under = "<" 30 | NotMore = "<=" 31 | 32 | # the specification of a version, release, or mask 33 | Release* = object 34 | case kind*: Operator 35 | of Tag: 36 | reference*: string 37 | of Wild, Caret, Tilde: 38 | accepts*: VersionMask 39 | of Equal, AtLeast, Over, Under, NotMore: 40 | version*: Version 41 | 42 | const 43 | Wildlings* = {Wild, Caret, Tilde} 44 | 45 | template starOrDigits(s: string): VersionMaskField = 46 | ## parse a star or digit as in a version mask 47 | if s == "*": 48 | # VersionMaskField is Option[VersionField] 49 | none(VersionField) 50 | else: 51 | some(s.parseUInt) 52 | 53 | proc parseDottedVersion*(input: string): Version = 54 | ## try to parse `1.2.3` into a `Version` 55 | let 56 | dotted = input.split('.') 57 | block: 58 | if dotted.len < 3: 59 | break 60 | try: 61 | result = (major: dotted[0].parseUInt, 62 | minor: dotted[1].parseUInt, 63 | patch: dotted[2].parseUInt) 64 | except ValueError: 65 | discard 66 | 67 | proc newVersionMask(input: string): VersionMask = 68 | ## try to parse `1.2` or `1.2.*` into a `VersionMask` 69 | let 70 | dotted = input.split('.') 71 | if dotted.len > 0: 72 | result.major = dotted[0].starOrDigits 73 | if dotted.len > 1: 74 | result.minor = dotted[1].starOrDigits 75 | if dotted.len > 2: 76 | result.patch = dotted[2].starOrDigits 77 | 78 | proc isValid*(release: Release): bool = 79 | ## true if the release seems plausible 80 | const sensible = @[ 81 | [ true, false, false], 82 | [ true, true, false], 83 | [ true, true, true ], 84 | ] 85 | case release.kind: 86 | of Tag: 87 | result = release.reference != "" 88 | of Wild, Caret, Tilde: 89 | let 90 | pattern = [release.accepts.major.isSome, 91 | release.accepts.minor.isSome, 92 | release.accepts.patch.isSome] 93 | result = pattern in sensible 94 | # let's say that *.*.* is valid; it could be useful 95 | if release.kind == Wild: 96 | result = result or pattern == [false, false, false] 97 | else: 98 | result = release.version.isValid 99 | 100 | proc newRelease*(version: Version): Release = 101 | ## create a new release using a version 102 | if not version.isValid: 103 | raise newException(ValueError, &"invalid version `{version}`") 104 | result = Release(kind: Equal, version: version) 105 | 106 | proc newRelease*(reference: string; operator = Equal): Release 107 | 108 | proc parseVersionLoosely*(content: string): Option[Release] = 109 | ## a very relaxed parser for versions found in tags, etc. 110 | ## only valid releases are emitted, however 111 | var 112 | release: Release 113 | let 114 | peggy = peg "document": 115 | ver <- +Digit * ('.' * +Digit)[0..2] 116 | record <- >ver * (!Digit | !1): 117 | if not release.isValid: 118 | release = newRelease($1, operator = Equal) 119 | document <- +(record | 1) * !1 120 | try: 121 | let 122 | parsed = peggy.match(content) 123 | if parsed.ok and release.isValid: 124 | result = release.some 125 | except Exception as e: 126 | let emsg = &"parse error in `{content}`: {e.msg}" # noqa 127 | warn emsg 128 | 129 | proc newRelease*(reference: string; operator = Equal): Release = 130 | ## parse a version, mask, or tag with an operator hint from the requirement 131 | if reference.startsWith("#") or operator == Tag: 132 | result = Release(kind: Tag, reference: reference) 133 | removePrefix(result.reference, {'#'}) 134 | elif reference in ["", "any version"]: 135 | result = Release(kind: Wild, accepts: newVersionMask("*")) 136 | elif "*" in reference: 137 | result = Release(kind: Wild, accepts: newVersionMask(reference)) 138 | elif operator in Wildlings: 139 | # thanks, jasper 140 | case operator: 141 | of Wildlings: 142 | result = Release(kind: operator, accepts: newVersionMask(reference)) 143 | else: 144 | raise newException(Defect, "inconceivable!") 145 | elif count(reference, '.') < 2: 146 | result = Release(kind: Wild, accepts: newVersionMask(reference)) 147 | else: 148 | result = newRelease(parseDottedVersion(reference)) 149 | 150 | proc `$`*(field: VersionMaskField): string = 151 | if field.isNone: 152 | result = "*" 153 | else: 154 | result = $field.get 155 | 156 | proc `$`*(mask: VersionMask): string = 157 | result = $mask.major 158 | result &= "." & $mask.minor 159 | result &= "." & $mask.patch 160 | 161 | proc omitStars*(mask: VersionMask): string = 162 | result = $mask.major 163 | if mask.minor.isSome: 164 | result &= "." & $mask.minor 165 | if mask.patch.isSome: 166 | result &= "." & $mask.patch 167 | 168 | proc `$`*(spec: Release): string = 169 | case spec.kind 170 | of Tag: 171 | result = $spec.kind & $spec.reference 172 | of Equal, AtLeast, Over, Under, NotMore: 173 | result = $spec.version 174 | of Wild, Caret, Tilde: 175 | result = spec.accepts.omitStars 176 | 177 | proc `==`*(a, b: VersionMaskField): bool = 178 | result = a.isNone == b.isNone 179 | if result and a.isSome: 180 | result = a.get == b.get 181 | 182 | proc `<`*(a, b: VersionMaskField): bool = 183 | result = a.isNone == b.isNone 184 | if result and a.isSome: 185 | result = a.get < b.get 186 | 187 | proc `==`*(a, b: VersionMask): bool = 188 | result = a.major == b.major 189 | result = result and a.minor == b.minor 190 | result = result and a.patch == b.patch 191 | 192 | proc `==`*(a, b: Release): bool = 193 | if a.kind == b.kind and a.isValid and b.isValid: 194 | case a.kind: 195 | of Tag: 196 | result = a.reference == b.reference 197 | of Wild, Caret, Tilde: 198 | result = a.accepts == b.accepts 199 | else: 200 | result = a.version == b.version 201 | 202 | proc `<`*(a, b: Release): bool = 203 | if a.kind == b.kind and a.isValid and b.isValid: 204 | case a.kind 205 | of Tag: 206 | result = a.reference < b.reference 207 | of Equal: 208 | result = a.version < b.version 209 | else: 210 | raise newException(ValueError, "inconceivable!") 211 | 212 | proc `<=`*(a, b: Release): bool = 213 | result = a == b or a < b 214 | 215 | proc `==`*(a: VersionMask; b: Version): bool = 216 | if a.major.isSome and a.major.get == b.major: 217 | if a.minor.isSome and a.minor.get == b.minor: 218 | if a.patch.isSome and a.patch.get == b.patch: 219 | result = true 220 | 221 | proc acceptable*(mask: VersionMaskField; op: Operator; 222 | value: VersionField): bool = 223 | ## true if the versionfield value passes the mask 224 | case op: 225 | of Wild: 226 | result = mask.isNone or value == mask.get 227 | of Caret: 228 | result = mask.isNone 229 | result = result or (value >= mask.get and mask.get > 0'u) 230 | result = result or (value == 0 and mask.get == 0) 231 | of Tilde: 232 | result = mask.isNone or value >= mask.get 233 | else: 234 | raise newException(Defect, "inconceivable!") 235 | 236 | proc at*[T: Version | VersionMask](version: T; index: VersionIndex): auto = 237 | ## like [int] but clashless 238 | case index: 239 | of 0: result = version.major 240 | of 1: result = version.minor 241 | of 2: result = version.patch 242 | 243 | proc `[]=`*(mask: var VersionMask; 244 | index: VersionIndex; value: VersionMaskField) = 245 | case index: 246 | of 0: mask.major = value 247 | of 1: mask.minor = value 248 | of 2: mask.patch = value 249 | 250 | iterator items*[T: Version | VersionMask](version: T): auto = 251 | for i in VersionIndex.low .. VersionIndex.high: 252 | yield version.at(i) 253 | 254 | iterator pairs*[T: Version | VersionMask](version: T): auto = 255 | for i in VersionIndex.low .. VersionIndex.high: 256 | yield (index: i, field: version.at(i)) 257 | 258 | proc isSpecific*(release: Release): bool = 259 | ## if the version/match specifies a full X.Y.Z version 260 | if release.kind in {Equal, AtLeast, NotMore} and release.isValid: 261 | result = true 262 | elif release.kind in Wildlings and release.accepts.patch.isSome: 263 | result = true 264 | 265 | proc specifically*(release: Release): Version = 266 | ## a full X.Y.Z version the release will match 267 | if not release.isSpecific: 268 | let emsg = &"release {release} is not specific" # noqa 269 | raise newException(Defect, emsg) 270 | if release.kind in Wildlings: 271 | result = (major: release.accepts.major.get, 272 | minor: release.accepts.minor.get, 273 | patch: release.accepts.patch.get) 274 | else: 275 | result = release.version 276 | 277 | proc effectively*(mask: VersionMask): Version = 278 | ## replace * with 0 in wildcard masks 279 | if mask.major.isNone: 280 | result = (0'u, 0'u, 0'u) 281 | elif mask.minor.isNone: 282 | result = (mask.major.get, 0'u, 0'u) 283 | elif mask.patch.isNone: 284 | result = (mask.major.get, mask.minor.get, 0'u) 285 | else: 286 | result = (mask.major.get, mask.minor.get, mask.patch.get) 287 | 288 | proc effectively*(release: Release): Version = 289 | ## convert a release to a version for rough comparisons 290 | case release.kind: 291 | of Tag: 292 | let parsed = parseVersionLoosely(release.reference) 293 | if parsed.isNone: 294 | result = (0'u, 0'u, 0'u) 295 | elif parsed.get.kind == Tag: 296 | raise newException(Defect, "inconceivable!") 297 | result = parsed.get.effectively 298 | of Wildlings: 299 | result = release.accepts.effectively 300 | of Equal: 301 | result = release.version 302 | else: 303 | raise newException(Defect, "not implemented") 304 | 305 | proc hash*(field: VersionMaskField): Hash = 306 | ## help hash version masks 307 | var h: Hash = 0 308 | if field.isNone: 309 | h = h !& '*'.hash 310 | else: 311 | h = h !& field.get.hash 312 | result = !$h 313 | 314 | proc hash*(mask: VersionMask): Hash = 315 | ## uniquely identify a version mask 316 | var h: Hash = 0 317 | h = h !& mask.major.hash 318 | h = h !& mask.minor.hash 319 | h = h !& mask.patch.hash 320 | result = !$h 321 | 322 | proc hash*(release: Release): Hash = 323 | ## uniquely identify a release 324 | var h: Hash = 0 325 | h = h !& release.kind.hash 326 | case release.kind: 327 | of Tag: 328 | h = h !& release.reference.hash 329 | of Wild, Tilde, Caret: 330 | h = h !& release.accepts.hash 331 | of Equal, AtLeast, Over, Under, NotMore: 332 | h = h !& release.version.hash 333 | result = !$h 334 | 335 | proc toMask*(version: Version): VersionMask = 336 | ## populate a versionmask with values from a version 337 | for i, field in version.pairs: 338 | result[i] = field.some 339 | 340 | proc importName*(target: Target): string = 341 | ## a uniform name usable in code for imports 342 | assert target.repo.len > 0 343 | result = target.repo.pathToImport.importName 344 | 345 | iterator likelyTags*(version: Version): string = 346 | ## produce tags with/without silly `v` prefixes 347 | let v = $version 348 | yield v 349 | yield "v" & v 350 | yield "V" & v 351 | yield "v." & v 352 | yield "V." & v 353 | 354 | iterator semanticVersionStrings*(mask: VersionMask): string = 355 | ## emit 3, 3.1, 3.1.4 (if possible) 356 | var 357 | last: string 358 | if mask.major.isSome: 359 | last = $mask.major.get 360 | yield last 361 | if mask.minor.isSome: 362 | last &= "." & $mask.minor.get 363 | yield last 364 | if mask.patch.isSome: 365 | yield last & "." & $mask.patch.get 366 | 367 | iterator semanticVersionStrings*(version: Version): string = 368 | ## emit 3, 3.1, 3.1.4 369 | yield $version.major 370 | yield $version.major & "." & $version.minor 371 | yield $version.major & "." & $version.minor & "." & $version.patch 372 | -------------------------------------------------------------------------------- /src/nimph/thehub.nim: -------------------------------------------------------------------------------- 1 | import std/tables 2 | import std/sequtils 3 | import std/httpclient 4 | import std/httpcore 5 | import std/json 6 | import std/os 7 | import std/options 8 | import std/asyncfutures 9 | import std/asyncdispatch 10 | import std/strutils 11 | import std/strformat 12 | import std/uri 13 | import std/times 14 | 15 | import rest 16 | import github 17 | import jsonconvert 18 | 19 | import nimph/spec 20 | import nimph/group 21 | 22 | const 23 | hubTime* = initTimeFormat "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'" 24 | 25 | type 26 | HubKind* = enum 27 | HubRelease 28 | HubTag 29 | HubCommit 30 | HubRepo 31 | HubIssue 32 | HubPull 33 | HubUser 34 | HubCode 35 | 36 | HubTree* = object 37 | sha*: string 38 | url*: Uri 39 | `type`*: string 40 | 41 | HubContact* = object 42 | name*: string 43 | email*: string 44 | date*: DateTime 45 | 46 | HubVerification* = object 47 | verified*: bool 48 | reason*: string 49 | signature*: string 50 | payload*: string 51 | 52 | HubCommitMeta* = object 53 | url*: Uri 54 | author*: HubContact 55 | committer*: HubContact 56 | message*: string 57 | commentCount*: int 58 | tree*: HubTree 59 | 60 | HubResult* = ref object 61 | htmlUrl*: Uri 62 | id*: int 63 | number*: int 64 | title*: string 65 | body*: string 66 | state*: string 67 | name*: string 68 | user*: HubResult 69 | tagName*: string 70 | targetCommitish*: string 71 | sha*: string 72 | created*: DateTime 73 | updated*: DateTime 74 | case kind*: HubKind: 75 | of HubCommit: 76 | tree*: HubTree 77 | author*: HubResult 78 | committer*: HubResult 79 | parents*: seq[HubTree] 80 | commit*: HubCommitMeta 81 | of HubTag: 82 | tagger*: HubContact 83 | `object`*: HubTree 84 | of HubRelease: 85 | draft*: bool 86 | prerelease*: bool 87 | of HubUser: 88 | login*: string 89 | of HubIssue: 90 | closedBy*: HubResult 91 | of HubPull: 92 | mergedBy*: HubResult 93 | merged*: bool 94 | of HubCode: 95 | path*: string 96 | repository*: HubResult 97 | of HubRepo: 98 | fullname*: string 99 | description*: string 100 | watchers*: int 101 | stars*: int 102 | forks*: int 103 | owner*: string 104 | size*: int 105 | pushed*: DateTime 106 | issues*: int 107 | clone*: Uri 108 | git*: Uri 109 | ssh*: Uri 110 | web*: Uri 111 | license*: string 112 | branch*: string 113 | original*: bool 114 | score*: float 115 | 116 | HubGroup* = ref object of Group[Uri, HubResult] 117 | 118 | HubSort* {.pure.} = enum 119 | Ascending = "asc" 120 | Descending = "desc" 121 | 122 | HubSortBy* {.pure.} = enum 123 | Best = "" 124 | Stars = "stars" 125 | Forks = "forks" 126 | Updated = "updated" 127 | 128 | proc shortly(stamp: DateTime): string = 129 | ## render a date shortly 130 | result = stamp.format(shortDate) 131 | 132 | proc renderShortly*(r: HubResult): string = 133 | result = &""" 134 | {r.web:<65} pushed {r.pushed.shortly} 135 | {r.size:>5} {"kb":<10} {r.issues:>4} {"issues":<10} {r.stars:>4} {"stars":<10} {r.forks:>4} {"forks":<10} created {r.created.shortly} 136 | {r.description} 137 | """ 138 | result = result.strip 139 | 140 | proc findGithubToken*(): Option[string] = 141 | ## find a github token in one of several places 142 | var 143 | token: string 144 | let 145 | hub = getHomeDir() / hubTokenFn 146 | file = getHomeDir() / dotNimble / ghTokenFn 147 | env = getEnv(ghTokenEnv, getEnv("GITHUB_TOKEN", getEnv("GHI_TOKEN", ""))) 148 | if env != "": 149 | token = env 150 | debug "using a github token from environment" 151 | elif fileExists(file): 152 | token = readFile(file) 153 | debug "using a github token from nimble" 154 | elif fileExists(hub): 155 | for line in lines(hub): 156 | if "oauth_token:" in line: 157 | token = line.strip.split(" ")[^1] 158 | debug "using a github token from hub" 159 | token = token.strip 160 | if token != "": 161 | result = token.some 162 | 163 | proc newHubContact*(js: JsonNode): HubContact = 164 | ## parse some json into a simple contact record 165 | let 166 | tz = utc() 167 | # 🐼 result = js.to(HubContact) 168 | if js == nil or "date" notin js or js.kind != JString: 169 | result = HubContact(date: now()) 170 | else: 171 | result = HubContact( 172 | date: js["date"].getStr.parse(hubTime, zone = tz) 173 | ) 174 | if js != nil: 175 | result.name = js.get("name", "") 176 | result.email = js.get("email", "") 177 | 178 | proc newHubTree*(js: JsonNode): HubTree = 179 | ## parse something like a commit tree 180 | result = HubTree() 181 | if js != nil: 182 | result.url = js.get("url", "").parseUri 183 | result.sha = js.get("sha", "") 184 | result.`type` = js.get("type", "") 185 | 186 | proc newHubCommitMeta*(js: JsonNode): HubCommitMeta = 187 | ## collect some ingredients found in a typical commit 188 | result = HubCommitMeta( 189 | committer: newHubContact js.getOrDefault("committer"), 190 | author: newHubContact js.getOrDefault("author") 191 | ) 192 | result.tree = newHubTree js.getOrDefault("tree") 193 | result.commentCount = js.get("comment_count", 0) 194 | 195 | proc newHubResult*(kind: HubKind; js: JsonNode): HubResult 196 | 197 | proc init*(result: var HubResult; js: JsonNode) = 198 | ## instantiate a new hub object using a jsonnode 199 | 200 | # impart a bit of sanity 201 | if js == nil or js.kind != JObject: 202 | raise newException(Defect, "nonsensical input: " & js.pretty) 203 | 204 | case result.kind: 205 | of HubRelease: discard 206 | of HubTag: discard 207 | of HubCommit: 208 | result.committer = HubUser.newHubResult(js["committer"]) 209 | result.author = HubUser.newHubResult(js["author"]) 210 | result.sha = js["sha"].getStr 211 | of HubIssue: 212 | if "closed_by" in js and js["closed_by"].kind == JObject: 213 | result.closedBy = HubUser.newHubResult(js["closed_by"]) 214 | of HubPull: 215 | result.merged = js.getOrDefault("merged").getBool 216 | if "merged_by" in js and js["merged_by"].kind == JObject: 217 | result.mergedBy = HubUser.newHubResult(js["merged_by"]) 218 | of HubCode: 219 | result.path = js.get("path", "") 220 | result.sha = js.get("sha", "") 221 | result.name = js.get("name", "") 222 | if "repository" in js: 223 | result.repository = HubRepo.newHubResult(js["repository"]) 224 | of HubRepo: 225 | result.fullname = js.get("full_name", "") 226 | result.owner = js.get("owner", "") 227 | result.name = js.get("name", "") 228 | result.description = js.get("description", "") 229 | result.stars = js.get("stargazers_count", 0) 230 | result.watchers = js.get("subscriber_count", 0) 231 | result.forks = js.get("forks_count", 0) 232 | result.issues = js.get("open_issues_count", 0) 233 | if "clone_url" in js: 234 | result.clone = js["clone_url"].getStr.parseUri 235 | if "git_url" in js: 236 | result.git = js["git_url"].getStr.parseUri 237 | if "ssh_url" in js: 238 | result.ssh = js["ssh_url"].getStr.parseUri 239 | if "homepage" in js and $js["homepage"] notin ["null", ""]: 240 | result.web = js["homepage"].getStr.parseUri 241 | if not result.web.isValid: 242 | result.web = result.htmlUrl 243 | if "license" in js: 244 | result.license = js["license"].getOrDefault("name").getStr 245 | result.branch = js.get("default_branch", "") 246 | result.original = not js.get("fork", false) 247 | result.score = js.get("score", 0.0) 248 | of HubUser: 249 | result.login = js.get("login", "") 250 | result.id = js.get("id", 0) 251 | if "title" in js: 252 | result.body = js.get("body", "") 253 | result.title = js.get("title", "") 254 | result.state = js.get("state", "") 255 | result.number = js.get("number", 0) 256 | if "user" in js and js["user"].kind == JObject: 257 | result.user = HubUser.newHubResult(js["user"]) 258 | 259 | proc newHubResult*(kind: HubKind; js: JsonNode): HubResult = 260 | # impart a bit of sanity 261 | if js == nil or js.kind != JObject: 262 | raise newException(Defect, "nonsensical input: " & js.pretty) 263 | 264 | template thenOrNow(label: string): DateTime = 265 | if js != nil and label in js and js[label].kind == JString: 266 | js[label].getStr.parse(hubTime, zone = tz) 267 | else: 268 | now() 269 | 270 | let 271 | tz = utc() 272 | kind = block: 273 | if "head" in js: 274 | HubPull 275 | elif kind == HubPull: 276 | HubIssue 277 | else: 278 | kind 279 | 280 | case kind 281 | of HubRelease: 282 | result = HubResult(kind: HubRelease, 283 | created: thenOrNow "created_at", 284 | updated: thenOrNow "updated_at") 285 | of HubTag: 286 | result = HubResult(kind: HubTag, 287 | tagger: newHubContact(js.getOrDefault("tagger")), 288 | `object`: newHubTree(js.getOrDefault("object")), 289 | created: thenOrNow "created_at", 290 | updated: thenOrNow "updated_at") 291 | of HubPull: 292 | result = HubResult(kind: HubPull, 293 | created: thenOrNow "created_at", 294 | updated: thenOrNow "updated_at") 295 | of HubCode: 296 | result = HubResult(kind: HubCode, 297 | created: thenOrNow "created_at", 298 | updated: thenOrNow "updated_at") 299 | of HubIssue: 300 | result = HubResult(kind: HubIssue, 301 | created: thenOrNow "created_at", 302 | updated: thenOrNow "updated_at") 303 | of HubRepo: 304 | result = HubResult(kind: HubRepo, 305 | pushed: thenOrNow "pushed_at", 306 | created: thenOrNow "created_at", 307 | updated: thenOrNow "updated_at") 308 | of HubCommit: 309 | result = HubResult(kind: HubCommit, 310 | commit: newHubCommitMeta(js.getOrDefault("commit")), 311 | created: thenOrNow "created_at", 312 | updated: thenOrNow "updated_at") 313 | of HubUser: 314 | result = HubResult(kind: HubUser, 315 | created: thenOrNow "created_at", 316 | updated: thenOrNow "updated_at") 317 | 318 | result.htmlUrl = js.get("html_url", "").parseUri 319 | result.init(js) 320 | 321 | proc newHubGroup*(flags: set[Flag] = defaultFlags): HubGroup = 322 | result = HubGroup(flags: flags) 323 | result.init(flags, mode = modeCaseSensitive) 324 | 325 | proc add*(group: var HubGroup; hub: HubResult) = 326 | {.warning: "nim bug #12818".} 327 | add[Uri, HubResult](group, hub.htmlUrl, hub) 328 | 329 | proc authorize*(request: Recallable): bool = 330 | ## find and inject credentials into a github request 331 | let token = findGithubToken() 332 | result = token.isSome 333 | if result: 334 | request.headers.del "Authorization" 335 | request.headers.add "Authorization", "token " & token.get 336 | else: 337 | error "unable to find a github authorization token" 338 | 339 | proc queryOne(recallable: Recallable; kind: HubKind): Future[Option[HubResult]] 340 | {.async.} = 341 | ## issue a recallable query and parse the response as a single item 342 | block success: 343 | # start with installing our credentials into the request 344 | if not recallable.authorize: 345 | break success 346 | 347 | # send the request to github and see if they like it 348 | let response = await recallable.issueRequest() 349 | if not response.code.is2xx: 350 | notice &"got response code {response.code} from github" 351 | break success 352 | 353 | # read the response and parse it to json 354 | let js = parseJson(await response.body) 355 | 356 | # turn the json into a hub result object 357 | result = newHubResult(kind, js).some 358 | 359 | proc queryMany(recallable: Recallable; kind: HubKind): Future[Option[HubGroup]] 360 | {.async.} = 361 | ## issue a recallable query and parse the response as a group of items 362 | block success: 363 | # start with installing our credentials into the request 364 | if not recallable.authorize: 365 | break success 366 | 367 | # send the request to github and see if they like it 368 | let response = await recallable.issueRequest() 369 | if not response.code.is2xx: 370 | notice &"got response code {response.code} from github" 371 | break success 372 | 373 | # read the response and parse it to json 374 | let js = parseJson(await response.body) 375 | 376 | # we know now that we'll be returning a group of some size 377 | var 378 | group = newHubGroup() 379 | result = group.some 380 | 381 | # add any parseable results to the group 382 | for node in js["items"].items: 383 | try: 384 | let item = newHubResult(kind, node) 385 | # if these are repositories, ignore forks 386 | if kind == HubRepo and not item.original: 387 | continue 388 | group.add item 389 | except Exception as e: 390 | warn "error parsing repo: " & e.msg 391 | 392 | proc getGitHubUser*(): Future[Option[HubResult]] {.async.} = 393 | ## attempt to retrieve the authorized user 394 | var 395 | req = getUser.call(_ = "") 396 | debug &"fetching github user" 397 | result = await req.queryOne(HubUser) 398 | 399 | proc forkHub*(owner: string; repo: string): Future[Option[HubResult]] {.async.} = 400 | ## attempt to fork an existing repository 401 | var 402 | req = postReposOwnerRepoForks.call(repo = repo, owner = owner, body = newJObject()) 403 | debug &"forking owner `{owner}` repo `{repo}`" 404 | result = await req.queryOne(HubRepo) 405 | 406 | proc searchHub*(keywords: seq[string]; sort = Best; 407 | order = Descending): Future[Option[HubGroup]] {.async.} = 408 | ## search github for packages 409 | var 410 | query = @["language:nim"].concat(keywords) 411 | req = getSearchRepositories.call(q = query.join(" "), 412 | sort = $sort, 413 | order = $order) 414 | debug &"searching github for {query}" 415 | result = await req.queryMany(HubRepo) 416 | 417 | when not defined(ssl): 418 | {.error: "this won't work without defining `ssl`".} 419 | -------------------------------------------------------------------------------- /src/nimph/doctor.nim: -------------------------------------------------------------------------------- 1 | import std/strtabs 2 | import std/tables 3 | import std/strutils 4 | import std/options 5 | import std/os 6 | import std/strformat 7 | 8 | import bump 9 | import gittyup 10 | 11 | import nimph/spec 12 | import nimph/project 13 | import nimph/nimble 14 | import nimph/config 15 | import nimph/thehub 16 | import nimph/package 17 | import nimph/dependency 18 | import nimph/group 19 | 20 | import nimph/requirement 21 | 22 | type 23 | StateKind* = enum 24 | DrOkay = "okay" 25 | DrRetry = "retry" 26 | DrError = "error" 27 | 28 | DrState* = object 29 | kind*: StateKind 30 | why*: string 31 | 32 | proc fixTags*(project: var Project; dry_run = true; force = false): bool = 33 | block: 34 | if project.dist != Git or not project.repoLockReady: 35 | info "not looking for missing tags because the repository is unready" 36 | break 37 | 38 | # you gotta spend money to make money 39 | project.fetchTagTable 40 | if project.tags == nil: 41 | notice "not looking for missing tags because i couldn't fetch any" 42 | break 43 | 44 | # we're gonna fetch the dump to make sure our version is sane 45 | if not project.fetchDump: 46 | notice "not looking for missing tags because my dump failed" 47 | break 48 | if "version" notin project.dump or project.dump["version"].count(".") > 2: 49 | notice &"refusing to tag {project.name} because its version is bizarre" 50 | break 51 | 52 | # open the repo so we can keep it in memory for tagging purposes 53 | repository := openRepository(project.gitDir): 54 | error &"unable to open repo at `{project.repo}`: {code.dumpError}" 55 | break 56 | 57 | # match up tags to versions to commits; we should probably 58 | # copy these structures and remove matches, for efficiency... 59 | var tagsNeeded = 0 60 | for version, commit in project.versionChangingCommits.pairs: 61 | block found: 62 | if $version in project.tags: 63 | let exists = project.tags[$version] 64 | debug &"found tag `{exists}` for {version}" 65 | break found 66 | for text, tag in project.tags.pairs: 67 | if commit.oid == tag.oid: 68 | debug &"found tag `{text}` for {version}" 69 | break found 70 | if dry_run: 71 | notice &"{project.name} is missing a tag for version {version}" 72 | info &"version {version} arrived in {commit}" 73 | result = true 74 | tagsNeeded.inc 75 | else: 76 | thing := repository.lookupThing($commit.oid): 77 | notice &"unable to lookup {commit}" 78 | continue 79 | # try to create a tag for this version and commit 80 | var 81 | nextTag = project.tags.nextTagFor(version) 82 | tagged = thing.tagCreate(nextTag, force = force) 83 | # first, try using the committer's signature 84 | if tagged.isErr: 85 | notice &"unable to create signed tag for {version}" 86 | # fallback to a lightweight (unsigned) tag 87 | tagged = thing.tagCreateLightweight(nextTag, force = force) 88 | if tagged.isErr: 89 | notice &"unable to create new tag for {version}" 90 | break found 91 | let 92 | oid = tagged.get 93 | # if that worked, let them know we did something 94 | info &"created new tag {version} as tag-{oid}" 95 | # the oid created for the tag must be freed 96 | dealloc oid 97 | 98 | # save our advice 'til the end 99 | if tagsNeeded > 0: 100 | notice "use the `tag` subcommand to add missing tags" 101 | 102 | proc fixDependencies*(project: var Project; group: var DependencyGroup; 103 | state: var DrState): bool = 104 | ## try to fix any outstanding issues with a set of dependencies 105 | 106 | # by default, everything is fine 107 | result = true 108 | # but don't come back here 109 | state.kind = DrError 110 | for requirement, dependency in group.mpairs: 111 | # if the dependency is being met, 112 | if dependency.isHappy: 113 | # but the version is not suitable, 114 | if not dependency.isHappyWithVersion: 115 | # try to roll any supporting project to a version that'll work 116 | for child in dependency.projects.mvalues: 117 | # if we're allowed to, i mean 118 | if Dry notin group.flags: 119 | # and if it was successful, 120 | if child.rollTowards(requirement): 121 | # report success 122 | notice &"rolled to {child.release} to meet {requirement}" 123 | break 124 | # else report the problem and set failure 125 | for req in requirement.orphans: 126 | if not req.isSatisfiedBy(child, child.release): 127 | notice &"{req.describe} unmet by {child}" 128 | result = false 129 | 130 | # the dependency is fine, but maybe we don't have it in our paths? 131 | for child in dependency.projects.mvalues: 132 | for path in project.missingSearchPaths(child): 133 | # report or update the paths 134 | if Dry in group.flags: 135 | notice &"missing path `{path}` in `{project.nimcfg}`" 136 | result = false 137 | elif project.addSearchPath(path): 138 | info &"added path `{path}` to `{project.nimcfg}`" 139 | # yay, we get to reload again 140 | project.cfg = loadAllCfgs(project.repo) 141 | else: 142 | warn &"couldn't add path `{path}` to `{project.nimcfg}`" 143 | result = false 144 | # dependency is happy and (probably) in a search path now 145 | continue 146 | 147 | # so i just came back from lunch and i was in the drive-thru and 148 | # reading reddit and managed to bump into the truck in front of me. 🙄 149 | # 150 | # this tiny guy pops out the door of the truck and practically tumbles 151 | # down the running board before arriving at the door to my car. he's 152 | # so short that all i can see is his little balled-up fist raised over 153 | # his head. 154 | # 155 | # i roll the window down, and he immediately yells, "I'M NOT HAPPY!" 156 | # to which my only possible reply was, "Well, which one ARE you, then?" 157 | # 158 | # anyway, if we made it this far, we're not happy... 159 | if Dry in group.flags: 160 | notice &"{dependency.name} ({requirement}) missing" 161 | result = false 162 | # for now, we'll force trying again even though it's a security risk, 163 | # because it will make users happy sooner, and we love happy users 164 | else: 165 | block cloneokay: 166 | for package in dependency.packages.mvalues: 167 | var cloned: Project 168 | if project.clone(package.url, package.name, cloned): 169 | if cloned.rollTowards(requirement): 170 | notice &"rolled to {cloned.release} to meet {requirement}" 171 | else: 172 | # we didn't roll, so we may need to relocate 173 | project.relocateDependency(cloned) 174 | state.kind = DrRetry 175 | break cloneokay 176 | else: 177 | error &"error cloning {package}" 178 | # a subsequent iteration could clone successfully 179 | # no package was successfully cloned 180 | notice &"unable to satisfy {requirement.describe}" 181 | result = false 182 | 183 | # okay, we did some stuff... let's see where we are now 184 | if state.kind == DrRetry: 185 | discard 186 | elif result: 187 | state.kind = DrOkay 188 | else: 189 | state.kind = DrError 190 | 191 | proc doctor*(project: var Project; dry = true; strict = true): bool = 192 | ## perform some sanity tests against the project and 193 | ## try to fix any issues we find unless `dry` is true 194 | var 195 | flags: set[Flag] = {} 196 | 197 | template toggle(x: typed; flag: Flag; test: bool) = 198 | if test: x.incl flag else: x.excl flag 199 | 200 | flags.toggle Dry, dry 201 | flags.toggle Strict, strict 202 | 203 | block configuration: 204 | debug "checking compiler configuration" 205 | let 206 | nimcfg = project.nimCfg 207 | # try a compiler parse of nim.cfg 208 | if not fileExists($nimcfg): 209 | # at the moment, we support any combination of local/user/global deps 210 | if false: 211 | # strictly speaking, this isn't a problem 212 | warn &"there wasn't a {NimCfg} in {project.nimble.repo}" 213 | if nimcfg.appendConfig("--clearNimblePath"): 214 | info "i created a new one" 215 | else: 216 | error "and i wasn't able to make a new one" 217 | else: 218 | let 219 | parsed = parseConfigFile($nimcfg) 220 | if parsed.isNone: 221 | error &"i had some issues trying to parse {nimcfg}" 222 | result = false 223 | 224 | # try a naive parse of nim.cfg 225 | if fileExists($project.nimCfg): 226 | let 227 | nimcfg = project.nimCfg 228 | parsed = parseProjectCfg(project.nimCfg) 229 | if not parsed.ok: 230 | error &"i had some issues trying to parse {nimcfg}:" 231 | error parsed.why 232 | result = false 233 | 234 | # try to parse all nim configuration files 235 | block globalconfig: 236 | when defined(debugPath): 237 | for path in project.cfg.likelySearch(libsToo = true): 238 | debug &"\tsearch: {path}" 239 | for path in project.cfg.likelyLazy: 240 | debug &"\t lazy: {path}" 241 | else: 242 | ## this space intentionally left blank 243 | 244 | block whoami: 245 | debug "checking project version" 246 | # check our project version 247 | let 248 | version = project.knowVersion 249 | # contextual errors are output by knowVersion 250 | result = version.isValid 251 | if result: 252 | debug &"{project.name} version {version}" 253 | 254 | block dependencies: 255 | debug "checking dependencies" 256 | # check our deps dir 257 | let 258 | depsDir = project.nimbleDir 259 | #absolutePath(project.nimble.repo / DepDir).normalizedPath 260 | envDir = getEnv("NIMBLE_DIR", "") 261 | if not dirExists(depsDir): 262 | info &"if you create {depsDir}, i'll use it for local dependencies" 263 | 264 | # $NIMBLE_DIR could screw with our head 265 | if envDir != "": 266 | if absolutePath(envDir) != depsDir: 267 | notice "i'm not sure what to do with an alternate $NIMBLE_DIR set" 268 | result = false 269 | else: 270 | info "your $NIMBLE_DIR is set, but it's set correctly" 271 | 272 | block checknimble: 273 | debug "checking nimble" 274 | # make sure nimble is a thing 275 | if findExe("nimble") == "": 276 | error "i can't find nimble in the path" 277 | result = false 278 | 279 | debug "checking nimble dump of our project" 280 | # make sure we can dump our project 281 | let 282 | damp = fetchNimbleDump(project.nimble.repo) 283 | if not damp.ok: 284 | error damp.why 285 | result = false 286 | else: 287 | project.dump = damp.table 288 | 289 | # see if we can find a github token 290 | block github: 291 | debug "checking for github token" 292 | let 293 | token = findGithubToken() 294 | if token.isNone: 295 | notice &"i wasn't able to discover a github token" 296 | warn &"please add a GitHub OAUTH token to your $NIMPH_TOKEN" 297 | result = false 298 | 299 | # see if git works 300 | block nimgit: 301 | if not gittyup.init(): 302 | error "i'm not able to initialize nimgit2 for git operations" 303 | result = false 304 | elif not gittyup.shutdown(): 305 | error "i'm not able to shutdown nimgit2 after initialization" 306 | result = false 307 | else: 308 | debug "git init/shut seems to be working" 309 | 310 | # see if we can get the packages list; try to refresh it if necessary 311 | block packages: 312 | while true: 313 | let 314 | packs = getOfficialPackages(project.nimbleDir) 315 | once: 316 | block skiprefresh: 317 | if not packs.ok: 318 | if packs.why != "": 319 | error packs.why 320 | notice &"couldn't get nimble's package list from {project.nimbleDir}" 321 | elif packs.ageInDays > stalePackages: 322 | notice &"the nimble package list in {project.nimbleDir} is stale" 323 | elif packs.ageInDays > 1: 324 | info "the nimble package list is " & 325 | &"{packs.ageInDays} days old" 326 | break skiprefresh 327 | else: 328 | break skiprefresh 329 | if not dry: 330 | let refresh = project.runSomething("nimble", @["refresh", "--accept"]) 331 | if refresh.ok: 332 | info "nimble refreshed the package list" 333 | continue 334 | result = false 335 | if packs.ok: 336 | let packages {.used.} = packs.packages 337 | debug &"loaded {packages.len} packages from nimble" 338 | break 339 | 340 | # check dependencies and maybe install some 341 | block dependencies: 342 | var 343 | group = project.newDependencyGroup(flags) 344 | state = DrState(kind: DrRetry) 345 | 346 | while state.kind == DrRetry: 347 | # we need to reload the config each repeat through this loop so that we 348 | # can correctly identify new search paths after adding new packages 349 | if not project.resolve(group): 350 | notice &"unable to resolve all dependencies for {project}" 351 | result = false 352 | state.kind = DrError 353 | elif not project.fixDependencies(group, state): 354 | result = false 355 | # maybe we're done here 356 | if state.kind notin {DrRetry}: 357 | break 358 | # we need to try again, but first we'll reset the environment 359 | fatal "👍environment changed; re-examining dependencies..." 360 | group.reset(project) 361 | 362 | # if dependencies are available via --nimblePath, then warn of any 363 | # dependencies that aren't recorded as part of the dependency graph; 364 | # this might be usefully toggled in spec. this should only issue a 365 | # warning if local deps exist or multiple nimblePaths are found 366 | block extradeps: 367 | if project.hasLocalDeps or project.numberOfNimblePaths > 1: 368 | let imports = project.cfg.allImportTargets(project.repo) 369 | for target, linked in imports.pairs: 370 | if group.isUsing(target): 371 | continue 372 | # ignore standard library targets 373 | if project.cfg.isStdLib(target.repo): 374 | continue 375 | let name = linked.importName 376 | warn &"no `{name}` requirement for {target.repo}" 377 | 378 | # identify packages that aren't named according to their versions; rename 379 | # local dependencies and merely warn about others 380 | {.warning: "mislabeled project directories unimplemented".} 381 | 382 | # remove missing paths from nim.cfg if possible 383 | block missingpaths: 384 | when defined(debugPath): 385 | for path in project.cfg.searchPaths.items: 386 | debug &"\tsearch: {path}" 387 | for path in project.cfg.lazyPaths.items: 388 | debug &"\t lazy: {path}" 389 | 390 | template cleanUpPathIn(form: string; iter: untyped): untyped = 391 | block complete: 392 | while true: 393 | block resume: 394 | for path in likelySearch(project.cfg, libsToo = false): 395 | if not dirExists(path): 396 | if dry: 397 | warn "$# path $# does not exist" % [ form, path ] 398 | result = false 399 | elif project.removeSearchPath(path): 400 | info "removed missing $# path $#" % [ form, path ] 401 | break resume 402 | elif excludeMissingSearchPaths and project.excludeSearchPath(path): 403 | info "excluded missing $# path $#" % [ form, path ] 404 | break resume 405 | else: 406 | warn "unable to remove $# path $#" % [ form, path ] 407 | result = false 408 | break complete 409 | 410 | # search paths that are missing should be removed/excluded 411 | cleanUpPathIn "search", likelySearch(project.cfg, libsToo = false) 412 | # lazy paths that are missing can be explicitly removed/ignored 413 | cleanUpPathIn "nimblePath", likelyLazy(project.cfg, least = 0) 414 | 415 | # if a dependency (local or otherwise) is shadowed by another dependency 416 | # in one of the nimblePaths, then we should warn that a removal of one 417 | # dep will default to the other 418 | # 419 | # if a dependency is shadowed with a manual path specification, we should 420 | # call that a proper error and offer to remove the weakest member 421 | # 422 | # we should calculate shadowing by name and version according to the way 423 | # the compiler compares versions 424 | block shadoweddeps: 425 | {.warning: "shadowed deps needs implementing".} 426 | 427 | # if a package exists and is local to the project and picked up by the 428 | # config (search paths or lazy paths) and it isn't listed in the 429 | # requirements, then we should warn about it 430 | block unspecifiedrequirement: 431 | {.warning: "unspecified requirements needs implementing".} 432 | 433 | # if a required packaged has a srcDir defined in the .nimble, then it needs to 434 | # be specified in the search paths 435 | block unspecifiedsearchpath: 436 | {.warning: "unspecified search path needs implementing".} 437 | 438 | # warn of tags missing for a particular version/commit pair 439 | block identifymissingtags: 440 | if project.fixTags(dry_run = true): 441 | result = false 442 | 443 | # warn if the user appears to have multiple --nimblePaths in use 444 | block nimblepaths: 445 | let 446 | found = project.countNimblePaths 447 | # don't distinguish between local or user lazy paths (yet) 448 | if found.local + found.global > 1: 449 | fatal "❔it looks like you have multiple --nimblePaths defined:" 450 | for index, path in found.paths.pairs: 451 | fatal &"❔\t{index + 1}\t{path}" 452 | fatal "❔nim and nimph support this, but some humans find it confusing 😏" 453 | -------------------------------------------------------------------------------- /src/nimph/config.nim: -------------------------------------------------------------------------------- 1 | import std/osproc 2 | import std/json 3 | import std/nre 4 | import std/strtabs 5 | import std/strformat 6 | import std/tables 7 | import std/os 8 | import std/options 9 | import std/strutils 10 | import std/algorithm 11 | 12 | import compiler/ast 13 | import compiler/idents 14 | import compiler/nimconf 15 | import compiler/options as compileropts 16 | import compiler/pathutils 17 | import compiler/condsyms 18 | import compiler/lineinfos 19 | 20 | export compileropts 21 | export nimconf 22 | 23 | import npeg 24 | import bump 25 | 26 | import nimph/spec 27 | import nimph/runner 28 | 29 | when defined(debugPath): 30 | from std/sequtils import count 31 | 32 | type 33 | ProjectCfgParsed* = object 34 | table*: TableRef[string, string] 35 | why*: string 36 | ok*: bool 37 | 38 | ConfigSection = enum 39 | LockerRooms = "lockfiles" 40 | 41 | NimphConfig* = ref object 42 | path: string 43 | js: JsonNode 44 | 45 | template excludeAllNotes(config: ConfigRef; n: typed) = 46 | config.notes.excl n 47 | when compiles(config.mainPackageNotes): 48 | config.mainPackageNotes.excl n 49 | when compiles(config.foreignPackageNotes): 50 | config.foreignPackageNotes.excl n 51 | 52 | template setDefaultsForConfig(result: ConfigRef) = 53 | # maybe we should turn off configuration hints for these reads 54 | when defined(debugPath): 55 | result.notes.incl hintPath 56 | elif not defined(debug): 57 | excludeAllNotes(result, hintConf) 58 | excludeAllNotes(result, hintLineTooLong) 59 | 60 | proc parseConfigFile*(path: string): Option[ConfigRef] = 61 | ## use the compiler to parse a nim.cfg without changing to its directory 62 | var 63 | cache = newIdentCache() 64 | filename = path.absolutePath 65 | config = newConfigRef() 66 | 67 | # define symbols such as, say, nimbabel; 68 | # this allows us to correctly parse conditions in nim.cfg(s) 69 | initDefines(config.symbols) 70 | 71 | setDefaultsForConfig(config) 72 | 73 | if readConfigFile(filename.AbsoluteFile, cache, config): 74 | result = some(config) 75 | 76 | when false: 77 | proc overlayConfig(config: var ConfigRef; 78 | directory: string): bool {.deprecated.} = 79 | ## true if new config data was added to the env 80 | withinDirectory(directory): 81 | var 82 | priorProjectPath = config.projectPath 83 | let 84 | nextProjectPath = AbsoluteDir getCurrentDir() 85 | filename = nextProjectPath.string / NimCfg 86 | 87 | block complete: 88 | # do not overlay above the current config 89 | if nextProjectPath == priorProjectPath: 90 | break complete 91 | 92 | # if there's no config file, we're done 93 | result = filename.fileExists 94 | if not result: 95 | break complete 96 | 97 | try: 98 | # set the new project path for substitution purposes 99 | config.projectPath = nextProjectPath 100 | 101 | var cache = newIdentCache() 102 | result = readConfigFile(filename.AbsoluteFile, cache, config) 103 | 104 | if result: 105 | # this config is now authoritative, so force the project path 106 | priorProjectPath = nextProjectPath 107 | else: 108 | let emsg = &"unable to read config in {nextProjectPath}" # noqa 109 | warn emsg 110 | finally: 111 | # remember to reset the config's project path 112 | config.projectPath = priorProjectPath 113 | 114 | # a global that we set just once per invocation 115 | var 116 | compilerPrefixDir: AbsoluteDir 117 | 118 | proc findPrefixDir(): AbsoluteDir = 119 | ## determine the prefix directory for the current compiler 120 | if compilerPrefixDir.isEmpty: 121 | debug "find prefix" 122 | let 123 | compiler = runSomething("nim", 124 | @["--hints:off", 125 | "--dump.format:json", "dump", "dummy"], {poDaemon}) 126 | if not compiler.ok: 127 | warn "couldn't run the compiler to determine its location" 128 | raise newException(OSError, "cannot find a nim compiler") 129 | try: 130 | let 131 | js = parseJson(compiler.output) 132 | compilerPrefixDir = AbsoluteDir js["prefixdir"].getStr 133 | except JsonParsingError as e: 134 | warn "`nim dump` json parse error: " & e.msg 135 | raise 136 | except KeyError: 137 | warn "couldn't parse the prefix directory from `nim dump` output" 138 | compilerPrefixDir = AbsoluteDir parentDir(findExe"nim") 139 | debug "found prefix" 140 | result = compilerPrefixDir 141 | 142 | proc loadAllCfgs*(directory: string): ConfigRef = 143 | ## use the compiler to parse all the usual nim.cfgs; 144 | ## optionally change to the given (project?) directory first 145 | 146 | result = newConfigRef() 147 | 148 | # define symbols such as, say, nimbabel; 149 | # this allows us to correctly parse conditions in nim.cfg(s) 150 | initDefines(result.symbols) 151 | 152 | setDefaultsForConfig(result) 153 | 154 | # stuff the prefixDir so we load the compiler's config/nim.cfg 155 | # just like the compiler would if we were to invoke it directly 156 | result.prefixDir = findPrefixDir() 157 | 158 | withinDirectory(directory): 159 | # stuff the current directory as the project path 160 | result.projectPath = AbsoluteDir getCurrentDir() 161 | 162 | # now follow the compiler process of loading the configs 163 | var cache = newIdentCache() 164 | 165 | # thanks, araq 166 | when (NimMajor, NimMinor) >= (1, 5): 167 | var idgen = IdGenerator() 168 | loadConfigs(NimCfg.RelativeFile, cache, result, idgen) 169 | else: 170 | loadConfigs(NimCfg.RelativeFile, cache, result) 171 | 172 | when defined(debugPath): 173 | debug "loaded", result.searchPaths.len, "search paths" 174 | debug "loaded", result.lazyPaths.len, "lazy paths" 175 | for path in result.lazyPaths.items: 176 | debug "\t", path 177 | for path in result.lazyPaths.items: 178 | if result.lazyPaths.count(path) > 1: 179 | raise newException(Defect, "duplicate lazy path: " & path.string) 180 | 181 | proc appendConfig*(path: Target; config: string): bool = 182 | # make a temp file in an appropriate spot, with a significant name 183 | let 184 | temp = createTemporaryFile(path.package, dotNimble) 185 | debug &"writing {temp}" 186 | # but remember to remove the temp file later 187 | defer: 188 | debug &"removing {temp}" 189 | if not tryRemoveFile(temp): 190 | warn &"unable to remove temporary file `{temp}`" 191 | 192 | block complete: 193 | try: 194 | # if there's already a config, we'll start there 195 | if fileExists($path): 196 | debug &"copying {path} to {temp}" 197 | copyFile($path, temp) 198 | except Exception as e: 199 | warn &"unable make a copy of {path} to to {temp}: {e.msg}" 200 | break complete 201 | 202 | block writing: 203 | # open our temp file for writing 204 | var 205 | writer = temp.open(fmAppend) 206 | try: 207 | # add our new content with a trailing newline 208 | writer.writeLine "# added by nimph:\n" & config 209 | finally: 210 | # remember to close the temp file in any event 211 | writer.close 212 | 213 | # make sure the compiler can parse our new config 214 | if parseConfigFile(temp).isNone: 215 | break complete 216 | 217 | # copy the temp file over the original config 218 | try: 219 | debug &"copying {temp} over {path}" 220 | copyFile(temp, $path) 221 | except Exception as e: 222 | warn &"unable make a copy of {temp} to to {path}: {e.msg}" 223 | break complete 224 | 225 | # it worked, thank $deity 226 | result = true 227 | 228 | proc parseProjectCfg*(input: Target): ProjectCfgParsed = 229 | ## parse a .cfg for any lines we are entitled to mess with 230 | result = ProjectCfgParsed(ok: false, table: newTable[string, string]()) 231 | var 232 | table = result.table 233 | 234 | block success: 235 | if not fileExists($input): 236 | result.why = &"config file {input} doesn't exist" 237 | break success 238 | 239 | var 240 | content = readFile($input) 241 | if not content.endsWith("\n"): 242 | content &= "\n" 243 | let 244 | peggy = peg "document": 245 | nl <- ?'\r' * '\n' 246 | white <- {'\t', ' '} 247 | equals <- *white * {'=', ':'} * *white 248 | assignment <- +(1 - equals) 249 | comment <- '#' * *(1 - nl) 250 | strvalue <- '"' * *(1 - '"') * '"' 251 | endofval <- white | comment | nl 252 | anyvalue <- +(1 - endofval) 253 | hyphens <- '-'[0..2] 254 | ending <- *white * ?comment * nl 255 | nimblekeys <- i"nimblePath" | i"clearNimblePath" | i"noNimblePath" 256 | otherkeys <- i"path" | i"p" | i"define" | i"d" 257 | keys <- nimblekeys | otherkeys 258 | strsetting <- hyphens * >keys * equals * >strvalue * ending: 259 | table.add $1, unescape($2) 260 | anysetting <- hyphens * >keys * equals * >anyvalue * ending: 261 | table.add $1, $2 262 | toggle <- hyphens * >keys * ending: 263 | table.add $1, "it's enabled, okay?" 264 | line <- strsetting | anysetting | toggle | (*(1 - nl) * nl) 265 | document <- *line * !1 266 | parsed = peggy.match(content) 267 | try: 268 | result.ok = parsed.ok 269 | if result.ok: 270 | break success 271 | result.why = parsed.repr 272 | except Exception as e: 273 | result.why = &"parse error in {input}: {e.msg}" 274 | 275 | proc isEmpty*(config: NimphConfig): bool = 276 | result = config.js.kind == JNull 277 | 278 | proc newNimphConfig*(path: string): NimphConfig = 279 | ## instantiate a new nimph config using the given path 280 | result = NimphConfig(path: path.absolutePath) 281 | if not result.path.fileExists: 282 | result.js = newJNull() 283 | else: 284 | try: 285 | result.js = parseFile(path) 286 | except Exception as e: 287 | error &"unable to parse {path}:" 288 | error e.msg 289 | 290 | template isStdLib*(config: ConfigRef; path: string): bool = 291 | path.startsWith(///config.libpath) 292 | 293 | template isStdlib*(config: ConfigRef; path: AbsoluteDir): bool = 294 | path.string.isStdLib 295 | 296 | iterator likelySearch*(config: ConfigRef; libsToo: bool): string = 297 | ## yield /-terminated directory paths likely added via --path 298 | for search in config.searchPaths.items: 299 | let 300 | search = ///search 301 | # we don't care about library paths 302 | if not libsToo and config.isStdLib(search): 303 | continue 304 | yield search 305 | 306 | iterator likelySearch*(config: ConfigRef; repo: string; libsToo: bool): string = 307 | ## yield /-terminated directory paths likely added via --path 308 | when defined(debug): 309 | if repo != repo.absolutePath: 310 | error &"repo {repo} wasn't normalized" 311 | 312 | for search in config.likelySearch(libsToo = libsToo): 313 | # limit ourselves to the repo? 314 | when WhatHappensInVegas: 315 | if search.startsWith(repo): 316 | yield search 317 | else: 318 | yield search 319 | 320 | iterator likelyLazy*(config: ConfigRef; least = 0): string = 321 | ## yield /-terminated directory paths likely added via --nimblePath 322 | # build a table of sightings of directories 323 | var popular = newCountTable[string]() 324 | for search in config.lazyPaths.items: 325 | let 326 | search = ///search 327 | parent = ///parentDir(search) 328 | when defined(debugPath): 329 | if search in popular: 330 | raise newException(Defect, "duplicate lazy path: " & search) 331 | if search notin popular: 332 | popular.inc search 333 | if search != parent: # silly: elide / 334 | if parent in popular: # the parent has to have been added 335 | popular.inc parent 336 | 337 | # sort the table in descending order 338 | popular.sort 339 | 340 | # yield the directories that exist 341 | for search, count in popular.pairs: 342 | # maybe we can ignore unpopular paths 343 | if least > count: 344 | continue 345 | yield search 346 | 347 | iterator likelyLazy*(config: ConfigRef; repo: string; least = 0): string = 348 | ## yield /-terminated directory paths likely added via --nimblePath 349 | when defined(debug): 350 | if repo != repo.absolutePath: 351 | error &"repo {repo} wasn't normalized" 352 | 353 | for search in config.likelyLazy(least = least): 354 | # limit ourselves to the repo? 355 | when WhatHappensInVegas: 356 | if search.startsWith(repo): 357 | yield search 358 | else: 359 | yield search 360 | 361 | iterator packagePaths*(config: ConfigRef; exists = true): string = 362 | ## yield package paths from the configuration as /-terminated strings; 363 | ## if the exists flag is passed, then the path must also exist. 364 | ## this should closely mimic the compiler's search 365 | 366 | # the method by which we de-dupe paths 367 | const mode = 368 | when FilesystemCaseSensitive: 369 | modeCaseSensitive 370 | else: 371 | modeCaseInsensitive 372 | var 373 | paths: seq[string] 374 | dedupe = newStringTable(mode) 375 | 376 | template addOne(p: AbsoluteDir) = 377 | let 378 | path = ///path 379 | if path in dedupe: 380 | continue 381 | dedupe[path] = "" 382 | paths.add path 383 | 384 | if config == nil: 385 | raise newException(Defect, "attempt to load search paths from nil config") 386 | 387 | for path in config.searchPaths: 388 | addOne(path) 389 | for path in config.lazyPaths: 390 | addOne(path) 391 | when defined(debugPath): 392 | debug &"package directory count: {paths.len}" 393 | 394 | # finally, emit paths as appropriate 395 | for path in paths: 396 | if exists and not path.dirExists: 397 | continue 398 | yield path 399 | 400 | proc suggestNimbleDir*(config: ConfigRef; local = ""; global = ""): string = 401 | ## come up with a useful nimbleDir based upon what we find in the 402 | ## current configuration, the location of the project, and the provided 403 | ## suggestions for local or global package directories 404 | var 405 | local = local 406 | global = global 407 | 408 | block either: 409 | # if a local directory is suggested, see if we can confirm its use 410 | if local != "" and local.dirExists: 411 | local = ///local 412 | assert local.endsWith(DirSep) 413 | for search in config.likelySearch(libsToo = false): 414 | if search.startsWith(local): 415 | # we've got a path statement pointing to a local path, 416 | # so let's assume that the suggested local path is legit 417 | result = local 418 | break either 419 | 420 | # nim 1.1.1 supports nimblePath storage in the config; 421 | # we follow a "standard" that we expect Nimble to use, 422 | # too, wherein the last-added --nimblePath wins 423 | when NimMajor >= 1 and NimMinor >= 1: 424 | if config.nimblePaths.len > 0: 425 | result = config.nimblePaths[0].string 426 | break either 427 | 428 | # otherwise, try to pick a global .nimble directory based upon lazy paths 429 | for search in config.likelyLazy: 430 | if search.endsWith(PkgDir & DirSep): 431 | result = search.parentDir # ie. the parent of pkgs 432 | else: 433 | result = search # doesn't look like pkgs... just use it 434 | break either 435 | 436 | # otherwise, try to make one up using the suggestion 437 | if global == "": 438 | raise newException(IOError, "can't guess global {dotNimble} directory") 439 | global = ///global 440 | assert global.endsWith(DirSep) 441 | result = global 442 | break either 443 | 444 | iterator pathSubsFor(config: ConfigRef; sub: string; conf: string): string = 445 | ## a convenience to work around the compiler's broken pathSubs; the `conf` 446 | ## string represents the path to the "current" configuration file 447 | block: 448 | if sub.toLowerAscii notin ["nimbledir", "nimblepath"]: 449 | yield ///config.pathSubs(&"${sub}", conf) 450 | break 451 | 452 | when declaredInScope nimbleSubs: 453 | for path in config.nimbleSubs(&"${sub}"): 454 | yield ///path 455 | else: 456 | # we have to pick the first lazy path because that's what Nimble does 457 | for search in config.lazyPaths: 458 | let 459 | search = ///search 460 | if search.endsWith(PkgDir & DirSep): 461 | yield ///parentDir(search) 462 | else: 463 | yield search 464 | break 465 | 466 | iterator pathSubstitutions(config: ConfigRef; path: string; 467 | conf: string; write: bool): string = 468 | ## compute the possible path substitions, including the original path 469 | const 470 | readSubs = @["nimcache", "config", "nimbledir", "nimblepath", 471 | "projectdir", "projectpath", "lib", "nim", "home"] 472 | writeSubs = 473 | when writeNimbleDirPaths: 474 | readSubs 475 | else: 476 | @["nimcache", "config", "projectdir", "lib", "nim", "home"] 477 | var 478 | matchedPath = false 479 | when defined(debug): 480 | if not conf.dirExists: 481 | raise newException(Defect, "passed a config file and not its path") 482 | let 483 | path = ///path 484 | conf = if conf.dirExists: conf else: conf.parentDir 485 | substitutions = if write: writeSubs else: readSubs 486 | 487 | for sub in substitutions.items: 488 | for attempt in config.pathSubsFor(sub, conf): 489 | # ignore any empty substitutions 490 | if attempt == "/": 491 | continue 492 | # note if any substitution matches the path 493 | if path == attempt: 494 | matchedPath = true 495 | if path.startsWith(attempt): 496 | yield path.replace(attempt, ///fmt"${sub}") 497 | # if a substitution matches the path, don't yield it at the end 498 | if not matchedPath: 499 | yield path 500 | 501 | proc bestPathSubstitution(config: ConfigRef; path: string; conf: string): string = 502 | ## compute the best path substitution, if any 503 | block found: 504 | for sub in config.pathSubstitutions(path, conf, write = true): 505 | result = sub 506 | break found 507 | result = path 508 | 509 | proc removeSearchPath*(config: ConfigRef; nimcfg: Target; path: string): bool = 510 | ## try to remove a path from a nim.cfg; true if it was 511 | ## successful and false if any error prevented success 512 | let 513 | fn = $nimcfg 514 | 515 | block complete: 516 | # well, that was easy 517 | if not fn.fileExists: 518 | break complete 519 | 520 | # make sure we can parse the configuration with the compiler 521 | if parseConfigFile(fn).isNone: 522 | error &"the compiler couldn't parse {nimcfg}" 523 | break complete 524 | 525 | # make sure we can parse the configuration using our "naive" npeg parser 526 | let 527 | parsed = nimcfg.parseProjectCfg 528 | if not parsed.ok: 529 | error &"could not parse {nimcfg} naïvely:" 530 | error parsed.why 531 | break complete 532 | 533 | # sanity 534 | when defined(debug): 535 | if path.absolutePath != path: 536 | raise newException(Defect, &"path `{path}` is not absolute") 537 | 538 | var 539 | content = fn.readFile 540 | # iterate over the entries we parsed naively, 541 | for key, value in parsed.table.pairs: 542 | # skipping anything that it's a path, 543 | if key.toLowerAscii notin ["p", "path", "nimblepath"]: 544 | continue 545 | # and perform substitutions to see if one might match the value 546 | # we are trying to remove; the write flag is false so that we'll 547 | # use any $nimbleDir substitutions available to us, if possible 548 | for sub in config.pathSubstitutions(path, nimcfg.repo, write = false): 549 | if sub notin [value, ///value]: 550 | continue 551 | # perform a regexp substition to remove the entry from the content 552 | let 553 | regexp = re("(*ANYCRLF)(?i)(?s)(-{0,2}" & key.escapeRe & 554 | "[:=]\"?" & value.escapeRe & "/?\"?)\\s*") 555 | swapped = content.replace(regexp, "") 556 | # if that didn't work, cry a bit and move on 557 | if swapped == content: 558 | notice &"failed regex edit to remove path `{value}`" 559 | continue 560 | # make sure we search the new content next time through the loop 561 | content = swapped 562 | result = true 563 | # keep performing more substitutions 564 | 565 | # finally, write the edited content 566 | fn.writeFile(content) 567 | 568 | proc addSearchPath*(config: ConfigRef; nimcfg: Target; path: string): bool = 569 | ## add the given path to the given config file, using the compiler's 570 | ## configuration as input to determine the best path substitution 571 | let 572 | best = config.bestPathSubstitution(path, $nimcfg.repo) 573 | result = appendConfig(nimcfg, &"""--path="{best}"""") 574 | 575 | proc excludeSearchPath*(config: ConfigRef; nimcfg: Target; path: string): bool = 576 | ## add an exclusion for the given path to the given config file, using the 577 | ## compiler's configuration as input to determine the best path substitution 578 | let 579 | best = config.bestPathSubstitution(path, $nimcfg.repo) 580 | result = appendConfig(nimcfg, &"""--excludePath="{best}"""") 581 | 582 | iterator extantSearchPaths*(config: ConfigRef; least = 0): string = 583 | ## yield existing search paths from the configuration as /-terminated strings; 584 | ## this will yield library paths and nimblePaths with at least `least` uses 585 | if config == nil: 586 | raise newException(Defect, "attempt to load search paths from nil config") 587 | # path statements 588 | for path in config.likelySearch(libsToo = true): 589 | if dirExists(path): 590 | yield path 591 | # nimblePath statements 592 | for path in config.likelyLazy(least = least): 593 | if dirExists(path): 594 | yield path 595 | 596 | proc addLockerRoom*(config: var NimphConfig; name: string; room: JsonNode) = 597 | ## add the named lockfile (in json form) to the configuration file 598 | if config.isEmpty: 599 | config.js = newJObject() 600 | if $LockerRooms notin config.js: 601 | config.js[$LockerRooms] = newJObject() 602 | config.js[$LockerRooms][name] = room 603 | writeFile(config.path, config.js.pretty) 604 | 605 | proc getAllLockerRooms*(config: NimphConfig): JsonNode = 606 | ## retrieve a JObject holding all lockfiles in the configuration file 607 | block found: 608 | if not config.isEmpty: 609 | if $LockerRooms in config.js: 610 | result = config.js[$LockerRooms] 611 | break 612 | result = newJObject() 613 | 614 | proc getLockerRoom*(config: NimphConfig; name: string): JsonNode = 615 | ## retrieve the named lockfile (or JNull) from the configuration 616 | let 617 | rooms = config.getAllLockerRooms 618 | if name in rooms: 619 | result = rooms[name] 620 | else: 621 | result = newJNull() 622 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nimph 2 | 3 | [![Test Matrix](https://github.com/disruptek/nimph/workflows/CI/badge.svg)](https://github.com/disruptek/nimph/actions?query=workflow%3ACI) 4 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/disruptek/nimph?style=flat)](https://github.com/disruptek/nimph/releases/latest) 5 | ![Minimum supported Nim version](https://img.shields.io/badge/nim-1.2.13%2B-informational?style=flat&logo=nim) 6 | [![License](https://img.shields.io/github/license/disruptek/nimph?style=flat)](#license) 7 | [![buy me a coffee](https://img.shields.io/badge/donate-buy%20me%20a%20coffee-orange.svg)](https://www.buymeacoffee.com/disruptek) 8 | 9 | nim package hierarchy manager from the future 10 | 11 | or: _How I Learned to Stop Worrying and Love the Search Path_ 12 | 13 | ## Features 14 | 15 | - truly path-agnostic dependencies 16 | - native git integration for speed 17 | - github api integration for comfort 18 | - reproducible builds via lockfiles 19 | - immutable cloud-based distributions 20 | - wildcard, tilde, and caret semver 21 | - absolutely zero configuration 22 | - total interoperability with Nimble 23 | - full-featured choosenim replacement 24 | 25 | ## Usage 26 | 27 | You can run `nimph` from anywhere in your project tree; it will simply search 28 | upwards until it finds a `.nimble` file and act as if you ran it there. 29 | 30 | Most operations do require that you be within a project, but `nimph` is 31 | flexible enough to operate on local dependencies, global packages, and anything 32 | in-between. You can run it on any package, anywhere, and it will provide useful 33 | output (and optional repair) of the environment it finds itself in. 34 | 35 | - [Searching for New Nim Packages](https://github.com/disruptek/nimph#search) 36 | - [Adding Packages to the Environment](https://github.com/disruptek/nimph#clone) 37 | - [Checking the Environment for Errors](https://github.com/disruptek/nimph#doctor) 38 | - [Quickly Forking an Installed Package](https://github.com/disruptek/nimph#fork) 39 | - [Finding a Path via Nim Import Name](https://github.com/disruptek/nimph#path) 40 | - [Locking the Dependency Tree by Name](https://github.com/disruptek/nimph#lock) 41 | - [Specifying Arbitrary Package Versions](https://github.com/disruptek/nimph#roll) 42 | - [Upgrading Dependencies Automatically](https://github.com/disruptek/nimph#upgrade) 43 | - [Downgrading Dependencies Automatically](https://github.com/disruptek/nimph#downgrade) 44 | - [Cutting New Release Versions+Tags](https://github.com/disruptek/nimph#bump) 45 | - [Adding Any Missing Tags Automatically](https://github.com/disruptek/nimph#tag) 46 | - [Running Commands on All Dependencies](https://github.com/disruptek/nimph#run) 47 | - [Outputting the Dependency Graph](https://github.com/disruptek/nimph#graph) 48 | - [Git Subcommand Auto-Integration](https://github.com/disruptek/nimph#git-subcommands) 49 | - [Nimble Subcommand Auto-Integration](https://github.com/disruptek/nimph#nimble-subcommands) 50 | - [Tweaking Nimph Behavior Constants](https://github.com/disruptek/nimph#hacking) 51 | - [Using `choosenim` to Select Nim Toolchains](https://github.com/disruptek/nimph#choose-nimph-choose-nim) 52 | - [Nimph Module Documentation](https://github.com/disruptek/nimph#documentation) 53 | 54 | ## Demonstration 55 | 56 | This is a demo screencast of using Nimph to setup a project for development. 57 | Starting with nothing more than the project's repository, we'll... 58 | 59 | 1. show the `bot.nimble` that specifies varied dependencies 60 | 1. show the `nim.cfg` that specifies compilation options 61 | 1. edit the `nim.cfg` to configure a directory to hold local dependencies 62 | 1. create a `deps` directory to hold those packages 63 | 1. run `nimph` to evaluate the state of the environment -- verdict: 😦 64 | 1. run `nimph doctor` to converge the environment to our specifications 65 | 1. run `nimph` to confirm the environment state -- verdict: 😊 66 | 1. show the `nim.cfg` to reveal any changes made by `nimph doctor` 67 | 68 | [![asciicast](https://asciinema.org/a/aoDAm39yjoKenepl15L3AyfzN.svg)](https://asciinema.org/a/aoDAm39yjoKenepl15L3AyfzN) 69 | 70 | ## Installation 71 | 72 | Some lucky few may be able to simply 73 | 74 | ``` 75 | nimble install https://github.com/disruptek/nimph 76 | ``` 77 | 78 | For the rest of us, bootstrap scripts are provided. 79 | 80 | ### Unix-like 81 | 82 | I recommend using the `bootstrap-nonimble.sh` script. If you prefer to use 83 | Nimble 😕 you can use the `bootstrap.sh` reproduced below; you'll see that it 84 | sets up a local dependency tree with which to build Nimph and its requirements. 85 | 86 | **nimterop must be able to find `cmake` in your $PATH** 87 | 88 | ```sh 89 | #!/bin/sh 90 | 91 | if ! test -f src/nimph.nim; then 92 | git clone --depth 1 git://github.com/disruptek/nimph.git 93 | cd nimph 94 | fi 95 | 96 | export NIMBLE_DIR="`pwd`/deps" 97 | mkdir "$NIMBLE_DIR" 98 | 99 | nimble --accept refresh 100 | nimble --accept install unicodedb@0.7.2 nimterop@0.6.11 101 | nimble install "--passNim:--path:\"`pwd`/src\" --outdir:\"`pwd`\"" 102 | 103 | if test -x nimph; then 104 | echo "nimph built successfully" 105 | else 106 | echo "unable to build nimph" 107 | exit 1 108 | fi 109 | ``` 110 | 111 | ### Windows 112 | 113 | I no longer test Windows via the CI because I have no way to debug Nimterop 114 | failures. That said, Windows builds may work just fine for you. 115 | 116 | To build Nimph on Windows, you need to have a working `cmake`. 117 | The easiest way to get it is via [scoop](https://scoop.sh/): 118 | 119 | ``` 120 | scoop install cmake 121 | ``` 122 | 123 | Here is the included `bootstrap.ps1`; as above, it simply sets up local 124 | dependencies before building Nimph. 125 | 126 | ```powershell 127 | if ( !(Join-Path 'src' 'nimph.nim' | Test-Path) ) { 128 | git clone git://github.com/disruptek/nimph.git 129 | Set-Location nimph 130 | } 131 | 132 | $env:NIMBLE_DIR = Join-Path $PWD 'deps' 133 | New-Item -Type Directory $env:NIMBLE_DIR -Force | Out-Null 134 | 135 | nimble --accept refresh 136 | nimble install "--passNim:--path:$(Resolve-Path 'src') --outDir:$PWD" 137 | ``` 138 | 139 | ### GitHub Integration 140 | 141 | You may want to [create a new GitHub personal access token 142 | here](https://github.com/settings/tokens) and then add it to your environment 143 | as `NIMPH_TOKEN` or `GITHUB_TOKEN`. 144 | 145 | If you skip this step, Nimph will try to use a Nimble token for **search**es, 146 | and it will also try to read any `hub` or `ghi` credentials. Notably, the 147 | **fork** subcommand will not work without adequate scope authorization. 148 | 149 | ## Subcommand Usage 150 | 151 | ### Search 152 | 153 | The `search` subcommand is used to query GitHub for 154 | packages. Arguments should match [GitHub search syntax for 155 | repositories](https://help.github.com/en/github/searching-for-information-on-gi 156 | thub/searching-for-repositories) and for convenience, a `language:nim` 157 | qualifier will be included. 158 | 159 | Results are output in **increasing order of relevance** to reduce scrolling; 160 | _the last result is the best_. 161 | 162 | ``` 163 | $ nimph search pegs 164 | 165 | https://github.com/GlenHertz/peg pushed 2017-11-19 166 | 645 kb 0 issues 0 stars 0 forks created 2017-11-18 167 | PEG version of grep 168 | 169 | https://github.com/lguzzon-NIM/simplePEG pushed 2019-09-05 170 | 82 kb 0 issues 0 stars 0 forks created 2017-09-05 171 | Simple Peg 172 | 173 | https://github.com/zevv/npeg pushed 2019-11-27 174 | 9125 kb 2 issues 66 stars 2 forks created 2019-03-08 175 | PEGs for Nim, another take 176 | ``` 177 | 178 | ### Clone 179 | 180 | The `clone` subcommand performs git clones to add packages to your environment. 181 | Pass this subcommand some GitHub search syntax and it will download the best 182 | matching package, or you can supply a URL directly. Local URLs are fine, too. 183 | 184 | Where the package ends up is a function of your existing compiler settings 185 | as recorded in relevant `nim.cfg` files; we'll search all `--nimblePath` 186 | statements, but according to a convention also adopted by Nimble... 187 | 188 | _The last specified --nimblePath, as processed by the `nim.cfg` files, is the 189 | "default" for the purposes of new package additions._ 190 | 191 | ``` 192 | $ nimph clone npeg 193 | 👭cloning git://github.com/zevv/npeg.git... 194 | 👌cloned git://github.com/zevv/npeg.git 195 | ``` 196 | 197 | ### Doctor 198 | 199 | The interesting action happens in the `doctor` subcommand. When run without any 200 | arguments, `nimph` effectively runs the `doctor` with a `--dry-run` option, to 201 | perform non-destructive evaluation of your environment and report any issues. 202 | In this mode, logging is elevated to report package versions and a summary of 203 | their last commit or tag. 204 | 205 | ``` 206 | $ nimph 207 | ✔️ 8a7114 bot cleanups 208 | ✔️ 775047 swayipc we can remove this notice now 209 | ✔️ v0.4.5 nesm Version 0.4.5 210 | ✔️ 5186f4 cligen Add a test program and update release notes as per last commit to fix https://github.com/c-blake/cligen/issues/120 211 | ✔️ c7ba0f dbus Merge pull request #3 from SolitudeSF/case 212 | ✔️ 57f244 c2nim new option: annotate procs with `{.noconv.}` 213 | ✔️ 54ed41 npeg Added section about non-consuming operators and captures to the README. Fixes #17 214 | ✔️ 183eaa unittest2 remove redundant import 215 | ✔️ v0.3.0 irc v0.3.0 216 | ✔️ fe276f rest add generated docs 217 | ✔️ 5d72a4 foreach clarify example 218 | ✔️ 5493b2 xs add some docs about google 219 | ✔️ 1.0.1 cutelog ladybug easier to see 220 | ✔️ 9d75fe bump update docs 221 | ✔️ 1.0.2 github fix nimble again 222 | ✔️ 6830ae nimph add asciinema demo 223 | ✔️ b6b8d5 compiler [backport] always set `fileInfoIdx.isKnownFile` (#12773) 224 | ✔️ v0.3.3 nimterop v0.3.3 225 | ✔️ v0.13.0 regex bump 0.13.0 (#52) 226 | ✔️ 2afc38 unicodedb improve decomposition performance (#11) 227 | ✔️ v0.5.1 unicodeplus Fix ascii range (#2) 228 | ✔️ v0.1.1 nimgit2 v0.1.1 229 | ✔️ v0.5.0 parsetoml Update to version 0.5.0 230 | 👌bot version 0.0.11 lookin' good 231 | ``` 232 | When run as `nimph doctor`, any problems discovered will be fixed, if possible. 233 | This includes cloning missing packages for which we can determine a URL, 234 | adjusting path settings in the project's `nim.cfg`, and similar housekeeping. 235 | 236 | ``` 237 | $ nimph doctor 238 | 👌bot version 0.0.11 lookin' good 239 | ``` 240 | 241 | ### Fork 242 | 243 | The `fork` subcommand is used to fork an installed dependency in your GitHub 244 | account and add a new git `origin` remote pointing at your new fork. The 245 | original `origin` remote is renamed to `upstream` by default. These constants 246 | may be easily changed; see **Hacking** below. 247 | 248 | This allows you to quickly move from merely testing a package to improving it 249 | and sharing your work upstream. 250 | 251 | ``` 252 | $ nimph fork npeg 253 | 🍴forking npeg-#54ed418e80f1e1b14133ed383b9c585b320a66cf 254 | 🔱https://github.com/disruptek/npeg 255 | ``` 256 | 257 | ### Path 258 | 259 | The `path` subcommand is used to retrieve the filesystem path to a package 260 | given the Nim symbol you might use to import it. For consistency, the package 261 | must be installed. 262 | 263 | In contrast to Nimble, you can specify multiple symbols to search for, and the 264 | symbols are matched without regard to underscores or capitalization. 265 | ``` 266 | $ nimph path nimterop irc 267 | /home/adavidoff/git/bot/deps/pkgs/nimterop-#v0.3.3 268 | /home/adavidoff/git/bot/deps/pkgs/irc-#v0.3.0 269 | ``` 270 | 271 | If you want to limit your search to packages that are part of your project's 272 | dependency tree, add the `--strict` switch: 273 | 274 | ``` 275 | $ nimph path coco 276 | /home/adavidoff/git/nimph/deps/pkgs/coco-#head 277 | 278 | $ nimph path --strict coco 279 | couldn't find a dependency importable as `coco` 280 | ``` 281 | 282 | It's useful to create a shell function to jump into dependency directories so 283 | you can quickly hack at them. 284 | 285 | ```bash 286 | #!/bin/bash 287 | function goto { pushd `nimph path $1`; } 288 | ``` 289 | 290 | or 291 | 292 | ```fish 293 | #!/bin/fish 294 | function goto; pushd (nimph path $argv); end 295 | ``` 296 | 297 | ### Lock 298 | 299 | The `lock` subcommand writes the current dependency tree to a JSON file; see 300 | **Hacking** below to customize its name. You pass arguments to give this record 301 | a name that you can use to retrieve the dependency tree later. Multiple such 302 | _lockfiles_ may be cached in a single file. 303 | 304 | ``` 305 | $ nimph lock works with latest npeg 306 | 👌locked nimph-#0.0.26 as `works with latest npeg` 307 | ``` 308 | 309 | ### Unlock 310 | 311 | The `unlock` subcommand reads a dependency tree previously saved with `lock` 312 | and adjusts the environment to match, installing any missing dependencies and 313 | rolling repositories to the versions that were recorded previously. 314 | 315 | ``` 316 | $ nimph unlock goats 317 | unsafe lock of `regex` for regex>=0.10.0 as #ff6ab8297c72f30e4da34daa9e8a60075ce8df7b 318 | 👭cloning https://github.com/zevv/npeg... 319 | rolled to #e3243f6ff2d05290f9c6f1e3d3f1c725091d60ab to meet git://github.com/disruptek/cutelog.git##1.1.1 320 | ``` 321 | 322 | ### Roll 323 | 324 | The `roll` subcommand lets you supply arbitrary requirements which are 325 | evaluated exactly as if they appeared in your package specification file. For 326 | shell escaping reasons, each such requirement should be a quoted string. 327 | 328 | ``` 329 | $ nimph roll "nimterop == 0.3.4" 330 | rolled to #v0.3.4 to meet nimterop>=0.3.3 331 | 👌nimph is lookin' good 332 | ``` 333 | 334 | Nimph will ensure that the new requirement doesn't break any existing 335 | requirements of the project or any of its dependencies. 336 | 337 | ``` 338 | $ nimph roll "nimterop > 6" 339 | nimterop*6 unmet by nimterop-#v0.3.4 340 | failed to fix all dependencies 341 | 👎nimph is not where you want it 342 | ``` 343 | 344 | As Nimble does not yet support caret (`^`), tilde (`~`), or wildcard (`*`), 345 | `roll` is the only way to experiment with these operators in requirements. 346 | 347 | ``` 348 | $ nimph roll "nimterop 0.3.*" 349 | rolled to #v0.3.6 to meet nimterop>=0.3.3 350 | 👌nimph is lookin' good 351 | ``` 352 | 353 | You can also use `roll` to resolve packages that are named in Nimble's official 354 | package directory but aren't hosted on GitHub. 355 | 356 | ``` 357 | $ nimph roll nesm 358 | 👭cloning https://gitlab.com/xomachine/NESM.git... 359 | rolled to #v0.4.5 to meet nesm** 360 | 👌xs is lookin' good 361 | ``` 362 | 363 | ### Upgrade 364 | 365 | The `upgrade` subcommand resolves the project's dependencies and attempts to 366 | upgrade any git clones to the latest release tag that matches the project's 367 | requirements. 368 | 369 | The `outdated` subcommand is an alias equivalent to `upgrade --dry-run`: 370 | 371 | ``` 372 | $ nimph outdated 373 | would upgrade bump from 1.8.16 to 1.8.17 374 | would upgrade nimph from 0.3.2 to 0.4.1 375 | would upgrade nimterop from 0.3.3 to v0.3.5 376 | 👎bot is not where you want it 377 | ``` 378 | 379 | Upgrade individual packages by specifying the _import name_. 380 | 381 | ``` 382 | $ nimph upgrade swayipc 383 | rolled swayipc from 3.1.0 to 3.1.3 384 | the latest swayipc release of 3.1.4 is masked 385 | 👌bot is up-to-date 386 | ``` 387 | 388 | Upgrade all dependencies at once by omitting any module names. 389 | 390 | ``` 391 | $ nimph upgrade 392 | the latest swayipc release of 3.1.4 is masked 393 | rolled foreach from 1.0.0 to 1.0.2 394 | rolled cutelog from 1.0.1 to 1.1.1 395 | rolled bump from 1.8.11 to 1.8.16 396 | rolled github from 1.0.1 to 1.0.2 397 | rolled nimph from 0.1.0 to 0.2.1 398 | rolled regex from 0.10.0 to v0.13.0 399 | rolled unicodedb from 0.6.0 to v0.7.2 400 | 👌bot is up-to-date 401 | ``` 402 | 403 | ### Downgrade 404 | 405 | The `downgrade` subcommand performs the opposite action to the upgrade 406 | subcommand. 407 | 408 | ``` 409 | $ nimph downgrade 410 | rolled swayipc from 3.1.4 to 3.1.0 411 | rolled cligen from 0.9.41 to v0.9.40 412 | rolled foreach from 1.0.2 to 1.0.0 413 | rolled cutelog from 1.1.1 to 1.0.1 414 | rolled bump from 1.8.16 to 1.8.11 415 | rolled github from 1.0.2 to 1.0.1 416 | rolled nimph from 0.3.2 to 0.3.0 417 | rolled regex from 0.13.0 to v0.10.0 418 | rolled unicodeplus from 0.5.1 to v0.5.0 419 | 👌bot is lookin' good 420 | ``` 421 | 422 | ### Bump 423 | 424 | The `bump` tool is included as a dependency; it provides easy version and tag incrementing. 425 | 426 | ``` 427 | $ bump fixed a bug 428 | 🎉1.0.3: fixed a bug 429 | 🍻bumped 430 | ``` 431 | 432 | For complete `bump` documentation, see https://github.com/disruptek/bump 433 | 434 | ### Tag 435 | 436 | The `tag` subcommand operates on a clean project and will roll the repository 437 | as necessary to examine any changes to your package configuration, noting any 438 | commits that: 439 | 440 | - introduced a new version of the package but aren't pointed to by a tag, _and_ 441 | - introduced a new version for which there exists no tag parsable as that version 442 | 443 | ``` 444 | $ nimph tag --dry-run --log-level=lvlInfo 445 | bump is missing a tag for version 1.1.0 446 | version 1.1.0 arrived in commit-009d45a977a688d22a9f1b14a21b6bd1a064760e 447 | use the `tag` subcommand to add missing tags 448 | run without --dry-run to fix these 449 | ``` 450 | 451 | The above conditions suggest that if you don't want to use this particular 452 | commit for your tag, you can simply point the tag at a different commit; Nimph 453 | won't change it on you. 454 | 455 | ``` 456 | $ git tag -a "re-release_of_1.1.0_just_in_time_for_the_holidays" 0abe7a9f0b5a05f2dd709f2b120805cc0cdd9668 457 | ``` 458 | 459 | Alternatively, if you don't want a version tag to be used by package managers, 460 | you can give the tag a name that won't parse as a version. Having found a tag 461 | for the commit, Nimph won't warn you that the commit needs tagging. 462 | 463 | ``` 464 | $ git tag -a "oops_this_was_compromised" 0abe7a9f0b5a05f2dd709f2b120805cc0cdd9668 465 | ``` 466 | 467 | When run without `--dry-run`, any missing tags are added automatically. 468 | 469 | ``` 470 | $ nimph tag --log-level=lvlInfo 471 | created new tag 1.1.0 for 009d45a977a688d22a9f1b14a21b6bd1a064760e 472 | 👌bump tags are lookin' good 473 | ``` 474 | 475 | Incidentally, these command-line examples demonstrate adjusting the log-level 476 | to increase verbosity. 477 | 478 | ### Run 479 | 480 | The `run` subcommand lets you invoke arbitrary programs in the root of each 481 | dependency of your project. 482 | 483 | ``` 484 | $ nimph run pwd 485 | /home/adavidoff/git/Nim 486 | /home/adavidoff/git/nimph/deps/pkgs/github-1.0.2 487 | /home/adavidoff/git/nimph/deps/pkgs/npeg-0.20.0 488 | /home/adavidoff/git/nimph/deps/pkgs/rest-#head 489 | /home/adavidoff/git/nimph/deps/pkgs/foreach-#head 490 | /home/adavidoff/git/nimph/deps/pkgs/cligen-#head 491 | /home/adavidoff/git/nimph/deps/pkgs/bump-1.8.15 492 | /home/adavidoff/git/nimph/deps/pkgs/cutelog-1.1.1 493 | /home/adavidoff/git/nimph/deps/pkgs/nimgit2-0.1.1 494 | /home/adavidoff/git/nimph/deps/pkgs/nimterop-0.3.3 495 | /home/adavidoff/git/nimph/deps/pkgs/regex-#v0.13.0 496 | /home/adavidoff/git/nimph/deps/pkgs/unicodedb-0.7.2 497 | /home/adavidoff/git/nimph/deps/pkgs/unicodeplus-0.5.0 498 | /home/adavidoff/git/nimph/deps/pkgs/unittest2-#head 499 | ``` 500 | 501 | To pass switches to commands `run` in your dependencies, use the `--` as a stopword. 502 | 503 | ``` 504 | $ nimph run -- head -1 LICENSE 505 | /bin/head: cannot open 'LICENSE' for reading: No such file or directory 506 | head -1 LICENSE 507 | head didn't like that in /home/adavidoff/git/Nim 508 | MIT License 509 | Copyright 2019 Ico Doornekamp 510 | MIT License 511 | MIT License 512 | Copyright (c) 2015,2016,2017,2018,2019 Charles L. Blake. 513 | MIT License 514 | MIT License 515 | MIT License 516 | MIT License 517 | MIT License 518 | MIT License 519 | MIT License 520 | /bin/head: cannot open 'LICENSE' for reading: No such file or directory 521 | head -1 LICENSE 522 | head didn't like that in /home/adavidoff/git/nimph/deps/pkgs/unittest2-#head 523 | ``` 524 | 525 | Finally, you can use the `--git` switch to limit `run` to dependencies with 526 | Git repositories; see [Git Subcommands](https://github.com/disruptek/nimph#git-subcommands) for examples. 527 | 528 | ### Graph 529 | 530 | The `graph` subcommand dumps some _very basic_ details about discovered 531 | dependencies and their associated packages and projects. 532 | 533 | ``` 534 | $ nimph graph 535 | 536 | requirement: swayipc>=3.1.4 from xs 537 | package: https://github.com/disruptek/swayipc 538 | 539 | requirement: cligen>=0.9.41 from xs 540 | requirement: cligen>=0.9.40 from bump 541 | package: https://github.com/c-blake/cligen.git 542 | directory: /home/adavidoff/.nimble/pkgs/cligen-0.9.41 543 | project: cligen-#b144d5b3392bac63ed49df3e1f176becbbf04e24 544 | 545 | requirement: dbus** from xs 546 | package: https://github.com/zielmicha/nim-dbus 547 | 548 | requirement: irc>=0.2.1 from xs 549 | package: https://github.com/nim-lang/irc 550 | 551 | requirement: https://github.com/disruptek/cutelog.git>=1.0.1 from xs 552 | requirement: git://github.com/disruptek/cutelog.git>=1.1.0 from bump 553 | package: git://github.com/disruptek/cutelog.git 554 | 555 | requirement: bump>=1.8.11 from xs 556 | package: file:///home/adavidoff/.nimble/pkgs/bump-1.8.13 557 | directory: /home/adavidoff/.nimble/pkgs/bump-1.8.13 558 | project: bump-1.8.13 559 | ``` 560 | 561 | Like other subcommands, you can provide _import names_ to retrieve the detail 562 | for only those dependencies, or omit any additional arguments to display all 563 | dependencies. 564 | 565 | ``` 566 | $ nimph graph cligen 567 | 568 | requirement: cligen>=0.9.41 from xs 569 | requirement: cligen>=0.9.40 from bump 570 | package: https://github.com/c-blake/cligen.git 571 | directory: /home/adavidoff/.nimble/pkgs/cligen-0.9.41 572 | project: cligen-#b144d5b3392bac63ed49df3e1f176becbbf04e24 573 | ``` 574 | 575 | Raising the log level of the `graph` command will cause retrieval and display 576 | releases and any _other_ commits at which the package changed versions. 577 | 578 | ``` 579 | $ nimph graph --log=lvlInfo nimterop 580 | 581 | requirement: nimterop>=0.3.3 from nimgit2 582 | package: https://github.com/genotrance/nimterop.git 583 | directory: /home/adavidoff/git/nimph/deps/pkgs/nimterop-0.4.0 584 | project: nimterop-#v0.4.0 585 | tagged release commits: 586 | tag: v0.1.0 commit-c3734587a174ea2fc7e19943e6d11d024f06e091 587 | tag: v0.2.0 commit-3e9dc2fb0fd6257fd86897c1b13f10ed2a5279b4 588 | tag: v0.2.1 commit-e9120eee7840851bda8113afbc71062b29fff872 589 | tag: v0.3.0 commit-37f5faa43d446a415e8934cc1a713bb7f5c5564f 590 | tag: v0.3.1 commit-1bca308ac472796329c212410ae198c0e31d3acb 591 | tag: v0.3.2 commit-12cc08900d1bfd39579164567acad75ca021a86b 592 | tag: v0.3.3 commit-751128e75859de66e07be9888c8341fe3b553816 593 | tag: v0.3.4 commit-c878a4be05cadd512db2182181b187de2a566ce8 594 | tag: v0.3.5 commit-c4b6a01878f0f72d428a24c26153723c60f6695f 595 | tag: v0.3.6 commit-d032a2c107d7f342df79980e01a3cf35194764de 596 | tag: v0.4.0 commit-f71cf837d297192f8cddfa136e8c3cd84bbc81eb 597 | untagged version commits: 598 | ver: 0.2.0 commit-3a2395360712d2c6f27221e0887b7e3cad0be7a1 599 | ver: 0.1.0 commit-9787797d15d281ce1dd792d247fac043c72dc769 600 | ``` 601 | 602 | ### Git Subcommands 603 | 604 | There are a couple shortcuts for running common git commands inside your 605 | dependencies: 606 | 607 | - `nimph fetch` is an alias for `nimph run -- git fetch`; ie. it runs `git fetch` in each dependency package directory. 608 | - `nimph pull` is an alias for `nimph run -- git pull`; ie. it runs `git pull` in each dependency package directory. 609 | 610 | ### Nimble Subcommands 611 | 612 | Any commands not mentioned above are passed directly to an instance of `nimble` 613 | which is run with the appropriate `nimbleDir` environment to ensure that it will 614 | operate upon the project it should. 615 | 616 | You can use this to, for example, **refresh** the official packages list, run **test**s, or build **doc**umentation for a project. 617 | 618 | ``` 619 | $ nimph refresh 620 | Downloading Official package list 621 | Success Package list downloaded. 622 | ``` 623 | 624 | ## Hacking 625 | 626 | Virtually all constants in Nimph are recorded in a single `spec` file where 627 | you can perform quick behavioral tweaks. Additionally, these constants may be 628 | overridden via `--define:key=value` statements during compilation. 629 | 630 | Notably, compiling `nimph` outside `release` or `danger` modes will increase 631 | the default log-level baked into the executable. Use a `debug` define for even 632 | more spam. 633 | 634 | Interesting procedures are exported so that you can exploit them in your own 635 | projects. 636 | 637 | Compilation flags to adjust output colors/styling/emojis are found in the 638 | project's `nimph.nim.cfg`. 639 | 640 | ## Choose Nimph, Choose Nim! 641 | 642 | The `choosenim` tool included in Nimph allows you to easily switch a symbolic 643 | link between adjacent Nim distributions, wherever you may have installed them. 644 | 645 | ### Installing `choosenim` 646 | 1. Install [jq](https://stedolan.github.io/jq/) from GitHub or wherever. 647 | 1. Add the `chosen` toolchain to your `$PATH`. 648 | 1. Run `choosenim` against any of your toolchains. 649 | ``` 650 | # after installing jq however you please... 651 | $ set --export PATH=/directory/for/all-my-nim-installations/chosen:$PATH 652 | $ ./choosenim 1.0 653 | Nim Compiler Version 1.0.7 [Linux: amd64] 654 | Compiled at 2020-04-05 655 | Copyright (c) 2006-2019 by Andreas Rumpf 656 | 657 | git hash: b6924383df63c91f0ad6baf63d0b1aa84f9329b7 658 | active boot switches: -d:release 659 | ``` 660 | 661 | ### Using `choosenim` 662 | To list available toolchains, run `choosenim`. 663 | ``` 664 | $ choosenim 665 | . 666 | ├── 1.0 667 | ├── 1.2 668 | ├── chosen -> 1.2 669 | ├── devel 670 | └── stable -> 1.0 671 | ``` 672 | Switch toolchains by supplying a name or alias. 673 | ``` 674 | $ choosenim 1.2 675 | Nim Compiler Version 1.2.0 [Linux: amd64] 676 | Compiled at 2020-04-05 677 | Copyright (c) 2006-2020 by Andreas Rumpf 678 | 679 | git hash: 7e83adff84be5d0c401a213eccb61e321a3fb1ff 680 | active boot switches: -d:release 681 | ``` 682 | ``` 683 | $ choosenim devel 684 | Nim Compiler Version 1.3.1 [Linux: amd64] 685 | Compiled at 2020-04-05 686 | Copyright (c) 2006-2020 by Andreas Rumpf 687 | 688 | git hash: b6814be65349d22fd12944c7c3d19fd8eb44683d 689 | active boot switches: -d:release 690 | ``` 691 | ``` 692 | $ choosenim stable 693 | Nim Compiler Version 1.0.7 [Linux: amd64] 694 | Compiled at 2020-04-05 695 | Copyright (c) 2006-2019 by Andreas Rumpf 696 | 697 | git hash: b6924383df63c91f0ad6baf63d0b1aa84f9329b7 698 | ``` 699 | 700 | ### Hacking `choosenim` 701 | It's a 20-line shell script, buddy; go nuts. 702 | 703 | ## Documentation 704 | 705 | See [the documentation for the nimph module](https://disruptek.github.io/nimph/nimph.html) as generated directly from the source. 706 | 707 | ## License 708 | MIT 709 | -------------------------------------------------------------------------------- /src/nimph/dependency.nim: -------------------------------------------------------------------------------- 1 | import std/uri 2 | import std/strformat 3 | import std/strutils 4 | import std/sets 5 | import std/hashes 6 | import std/strtabs 7 | import std/tables 8 | import std/options 9 | import std/sequtils 10 | import std/algorithm 11 | 12 | import bump 13 | import gittyup 14 | 15 | import nimph/spec 16 | import nimph/package 17 | import nimph/project 18 | import nimph/version 19 | import nimph/versiontags 20 | import nimph/requirement 21 | import nimph/config 22 | 23 | import nimph/group 24 | export group 25 | 26 | type 27 | Dependency* = ref object 28 | names*: seq[string] 29 | requirement*: Requirement 30 | packages*: PackageGroup 31 | projects*: ProjectGroup 32 | 33 | DependencyGroup* = ref object of Group[Requirement, Dependency] 34 | packages*: PackageGroup 35 | projects*: ProjectGroup 36 | 37 | proc name*(dependency: Dependency): string = 38 | result = dependency.names.join("|") 39 | 40 | proc `$`*(dependency: Dependency): string = 41 | result = dependency.name & "->" & $dependency.requirement 42 | 43 | proc newDependency*(requirement: Requirement): Dependency = 44 | result = Dependency(requirement: requirement) 45 | result.projects = newProjectGroup() 46 | result.packages = newPackageGroup() 47 | 48 | proc newDependencyGroup*(flags: set[Flag]): DependencyGroup = 49 | result = DependencyGroup(flags: flags) 50 | result.init(flags, mode = modeStyleInsensitive) 51 | 52 | proc contains*(dependencies: DependencyGroup; package: Package): bool = 53 | ## true if the package's url matches that of a package in the group 54 | for name, dependency in dependencies.pairs: 55 | result = dependency.packages.hasUrl(package.url) 56 | if result: 57 | break 58 | 59 | proc hasKey*(dependencies: DependencyGroup; name: string): bool = 60 | result = dependencies.imports.hasKey(name) 61 | 62 | proc reportMultipleResolutions(project: Project; requirement: Requirement; 63 | packages: PackageGroup) = 64 | ## output some useful warnings depending upon the nature of the dupes 65 | var 66 | urls: HashSet[Hash] 67 | for url in packages.urls: 68 | urls.incl url.hash 69 | 70 | # if the packages all share the same url, we can simplify the output 71 | if urls.len == 1: 72 | warn &"{project.name} has {packages.len} " & 73 | &"options for {requirement} dependency, all via" 74 | for url in packages.urls: 75 | warn &"\t{url}" 76 | break 77 | # otherwise, we'll emit urls for each package as well 78 | else: 79 | warn &"{project.name} has {packages.len} " & 80 | &"options for {requirement} dependency:" 81 | # output a line or two for each package 82 | var count = 1 83 | for package in packages.values: 84 | if package.local: 85 | warn &"\t{count}\t{package.path}" 86 | elif package.web.isValid: 87 | warn &"\t{count}\t{package.web}" 88 | if urls.len != 1: 89 | warn &"\t{package.url}\n" 90 | count.inc 91 | 92 | proc asPackage*(project: Project): Package = 93 | ## cast a project to a package; this is used to seed the packages list 94 | ## for projects that are already installed so that we can match them 95 | ## against other package metadata sources by url, etc. 96 | result = newPackage(name = project.name, path = project.repo, 97 | dist = project.dist, url = project.createUrl()) 98 | 99 | proc adopt*(parent: Project; child: var Project) = 100 | ## associate a child project with the parent project of which the 101 | ## child is a requirement, member of local dependencies, or otherwise 102 | ## available to the compiler's search paths 103 | if child.parent != nil and child.parent != parent: 104 | let emsg = &"{parent} cannot adopt {child}" 105 | raise newException(Defect, emsg) 106 | child.parent = parent 107 | 108 | proc childProjects*(project: Project): ProjectGroup = 109 | ## compose a group of possible dependencies of the project; in fact, 110 | ## this will include literally any project in the search paths 111 | result = project.availableProjects 112 | for child in result.mvalues: 113 | if child == project: 114 | continue 115 | project.adopt(child) 116 | discard child.fetchConfig 117 | 118 | proc determineDeps*(project: Project): Option[Requires] = 119 | ## try to parse requirements of a project using the `nimble dump` output 120 | block: 121 | if project.dump == nil: 122 | error "unable to determine deps without issuing a dump" 123 | break 124 | result = parseRequires(project.dump["requires"]) 125 | if result.isNone: 126 | break 127 | # this is (usually) gratuitous, but it's also the right place 128 | # to perform this assignment, so... go ahead and do it 129 | for a, b in result.get.mpairs: 130 | a.notes = project.name 131 | b.notes = project.name 132 | 133 | proc determineDeps*(project: var Project): Option[Requires] = 134 | ## try to parse requirements of a project using the `nimble dump` output 135 | if not project.fetchDump: 136 | debug "nimble dump failed, so computing deps is impossible" 137 | else: 138 | let 139 | readonly = project 140 | result = determineDeps(readonly) 141 | 142 | proc peelRelease*(project: Project; release: Release): Release = 143 | ## peel a release, if possible, to resolve any tags as commits 144 | # default to just returning the release we were given 145 | result = release 146 | 147 | block: 148 | # if there's no way to peel it, just bail 149 | if project.dist != Git or result.kind != Tag: 150 | break 151 | 152 | # else, open the repo 153 | repository := openRepository(project.gitDir): 154 | error &"unable to open repo at `{project.repo}`: {code.dumpError}" 155 | break 156 | 157 | # and look up the reference 158 | thing := repository.lookupThing(result.reference): 159 | warn &"unable to find release reference `{result.reference}`" 160 | break 161 | 162 | # it's a valid reference, let's try to convert it to a release 163 | case thing.kind: 164 | of goTag: 165 | # the reference is a tag, so we need to resolve the target oid 166 | result = project.peelRelease newRelease($thing.targetId, 167 | operator = Tag) 168 | of goCommit: 169 | # good; we found a matching commit 170 | result = newRelease($thing.oid, operator = Tag) 171 | else: 172 | # otherwise, it's some kinda git object we don't grok 173 | let emsg = &"{thing.kind} references unimplemented" # noqa 174 | raise newException(ValueError, emsg) 175 | 176 | proc peelRelease*(project: Project): Release = 177 | ## convenience to peel the project's release 178 | result = project.peelRelease(project.release) 179 | 180 | proc happyProvision(requirement: Requirement; release: Release; 181 | head = ""; tags: GitTagTable = nil): bool = 182 | ## true if the requirement (and children) are satisfied by the release 183 | var 184 | req = requirement 185 | 186 | block failed: 187 | while req != nil: 188 | if req.release.kind == Tag: 189 | let 190 | required = releaseHashes(req.release, head = head) 191 | block matched: 192 | for viable, thing in tags.matches(required, head = head): 193 | # the requirement is a tag, so we simply compare the 194 | # matches for the requirement against the provided release 195 | # and a release composed of each match's commit hash 196 | let candidate = newRelease($thing.oid, operator = Tag) 197 | if release in [viable, candidate]: 198 | break matched 199 | break failed 200 | elif not req.isSatisfiedBy(release): 201 | # the release hashes are provided, literally 202 | let 203 | provided = releaseHashes(release, head = head) 204 | block matched: 205 | for viable, thing in tags.matches(provided, head = head): 206 | if req.isSatisfiedBy(viable): 207 | break matched 208 | break failed 209 | req = req.child 210 | result = true 211 | 212 | iterator matchingReleases(requirement: Requirement; head = ""; 213 | tags: GitTagTable): Release = 214 | ## yield releases that satisfy the requirement, using the head and tags 215 | # we need to keep track if we've seen the head oid, because 216 | # we don't want to issue it twice 217 | var 218 | sawTheHead = false 219 | 220 | if tags != nil: 221 | for tag, thing in tags.pairs: 222 | let 223 | release = newRelease($thing.oid, operator = Tag) 224 | if requirement.happyProvision(release, head = head, tags = tags): 225 | sawTheHead = sawTheHead or $thing.oid == head 226 | yield release 227 | 228 | if head != "" and not sawTheHead: 229 | let 230 | release = newRelease(head, operator = Tag) 231 | if requirement.happyProvision(release, head = head, tags = tags): 232 | yield release 233 | 234 | iterator symbolicMatch*(project: Project; req: Requirement): Release = 235 | ## see if a project can match a given requirement symbolically 236 | if project.dist == Git: 237 | if project.tags == nil: 238 | warn &"i wanted to examine tags for {project} but they were empty" 239 | raise newException(Defect, "seems like a programmer error to me") 240 | let 241 | oid = project.demandHead 242 | for release in req.matchingReleases(head = oid, tags = project.tags): 243 | debug &"release match {release} for {req}" 244 | yield release 245 | # here we will try to lookup any random reference requirement, just in case 246 | # 247 | # this currently could duplicate a release emitted above, but that's okay 248 | if req.release.kind == Tag: 249 | block: 250 | # try to find a matching oid in the current branch 251 | for branch in project.matchingBranches(req.release.reference): 252 | debug &"found {req.release.reference} in {project}" 253 | yield newRelease($branch.oid, operator = Tag) 254 | repository := openRepository(project.gitDir): 255 | error &"unable to open repo at `{project.repo}`: {code.dumpError}" 256 | break 257 | # else, it's a random oid, maybe? look it up! 258 | thing := repository.lookupThing(req.release.reference): 259 | debug &"could not find {req.release.reference} in {project}" 260 | break 261 | debug &"found {req.release.reference} in {project}" 262 | yield newRelease($thing.oid, operator = Tag) 263 | else: 264 | debug &"without a repo for {project.name}, i cannot match {req}" 265 | # if we don't have any tags or the head, it's a simple test 266 | if req.isSatisfiedBy(project.release): 267 | yield project.release 268 | 269 | proc symbolicMatch*(project: Project; req: Requirement; release: Release): bool = 270 | ## convenience 271 | let release = project.peelRelease(release) 272 | for match in project.symbolicMatch(req): 273 | result = match == release 274 | if result: 275 | break 276 | 277 | proc symbolicMatch*(project: var Project; req: Requirement; release: Release): bool = 278 | ## convenience that fetches the tag table if necessary 279 | if project.tags == nil: 280 | project.fetchTagTable 281 | let readonly = project 282 | result = readonly.symbolicMatch(req, release) 283 | 284 | proc symbolicMatch*(project: Project; req: Requirement): bool = 285 | ## convenience 286 | for match in project.symbolicMatch(req): 287 | result = true 288 | break 289 | 290 | proc symbolicMatch*(project: var Project; req: Requirement): bool = 291 | ## convenience that fetches the tag table if necessary 292 | if project.tags == nil: 293 | project.fetchTagTable 294 | let readonly = project 295 | result = readonly.symbolicMatch(req) 296 | 297 | proc isSatisfiedBy*(req: Requirement; project: Project; release: Release): bool = 298 | ## true if the requirement is satisfied by the project at the given release 299 | block satisfied: 300 | if project.dist == Git: 301 | if project.tags == nil: 302 | raise newException(Defect, "really expected to have tags here") 303 | 304 | # match loosely on tag, version, etc. 305 | let 306 | oid = project.demandHead 307 | for match in req.matchingReleases(head = oid, tags = project.tags): 308 | result = release == match 309 | if result: 310 | break satisfied 311 | 312 | # this is really our last gasp opportunity for a Tag requirement 313 | if req.release.kind == Tag: 314 | # match against a specific oid or symbol 315 | if release.kind == Tag: 316 | # try to find a matching branch name 317 | for branch in project.matchingBranches(req.release.reference): 318 | result = true 319 | break satisfied 320 | 321 | block: 322 | repository := openRepository(project.gitDir): 323 | error &"unable to open repo at `{project.repo}`: {code.dumpError}" 324 | break 325 | thing := repository.lookupThing(name = release.reference): 326 | notice &"tag/oid `{release.reference}` in {project.name}: {code}" 327 | break 328 | debug &"release reference {release.reference} is {thing}" 329 | result = release.reference == req.release.reference 330 | break satisfied 331 | 332 | # basically, this could work if we've pulled a tag from nimblemeta 333 | if req.release.kind == Tag: 334 | result = req.release.reference == release.reference 335 | break satisfied 336 | 337 | # otherwise, if all we have is a tag but the requirement is for 338 | # something version-like, then we have to just use the version; 339 | # ditto if we don't even have a valid release, of course 340 | if not release.isValid or release.kind == Tag: 341 | # we should only do this if we're trying to solve the project release 342 | assert release == project.release 343 | if project.version.isValid: 344 | # fallback to the version indicated by nimble 345 | result = req.isSatisfiedBy newRelease(project.version) 346 | debug &"project version match {result} {req}" 347 | # we did our best 348 | break 349 | 350 | # the release is valid and it's not a tag. 351 | # 352 | # first we wanna see if our version is specific; if it is, we 353 | # will just see if that specific incarnation satisfies the req 354 | if release.isSpecific: 355 | # try to use our release 356 | result = req.isSatisfiedBy newRelease(release.specifically) 357 | debug &"release match {result} {req}" 358 | break satisfied 359 | 360 | # make sure we can satisfy prior requirements as well 361 | if result and req.child != nil: 362 | result = req.child.isSatisfiedBy(project, release) 363 | 364 | proc isSatisfiedBy*(req: Requirement; project: Project): bool = 365 | ## true if a requirement is satisfied by the given project, 366 | ## at any known/available version for the project 367 | # first, check that the identity matches 368 | if project.name == req.identity: 369 | result = true 370 | elif req.isUrl: 371 | let 372 | url = req.toUrl 373 | if url.isSome: 374 | let 375 | x = project.url.convertToGit 376 | y = url.get.convertToGit 377 | result = x == y or bareUrlsAreEqual(x, y) 378 | # if the name doesn't match, let's just bomb early 379 | if not result: 380 | return 381 | # now we need to confirm that the version will work 382 | result = block: 383 | # if the project's release satisfies the requirement, great 384 | if req.isSatisfiedBy(project, project.release): 385 | true 386 | # it's also fine if the project can symbolically satisfy the requirement 387 | elif project.symbolicMatch(req): 388 | true 389 | # else we really have no reason to think we can satisfy the requirement 390 | else: 391 | false 392 | 393 | # make sure we can satisfy prior requirements as well 394 | if result and req.child != nil: 395 | result = req.child.isSatisfiedBy(project) 396 | 397 | {.warning: "nim bug #12818".} 398 | proc get*[K: Requirement, V](group: Group[K, V]; key: Requirement): V = 399 | ## fetch a dependency from the group using the requirement 400 | result = group.table[key] 401 | 402 | proc mget*[K: Requirement, V](group: var Group[K, V]; key: K): var V = 403 | ## fetch a dependency from the group using the requirement 404 | result = group.table[key] 405 | 406 | proc addName(dependency: var Dependency; name: string) = 407 | ## add an import name to the dependency, as might be used in code 408 | let 409 | package = name.importName.toLowerAscii 410 | if package notin dependency.names: 411 | dependency.names.add package 412 | 413 | proc add(dependency: var Dependency; package: Package) = 414 | ## add a package to the dependency 415 | if package.url notin dependency.packages: 416 | dependency.packages.add package.url, package 417 | dependency.addName package.name 418 | 419 | proc add(dependency: var Dependency; url: Uri) = 420 | ## add a url (as a package) to the dependency 421 | dependency.add newPackage(url = url) 422 | 423 | proc add(dependency: var Dependency; packages: PackageGroup) = 424 | ## add a group of packages to the dependency 425 | for package in packages.values: 426 | dependency.add package 427 | 428 | proc add(dependency: var Dependency; directory: string; project: Project) = 429 | ## add a local project in the given directory to an existing dependency 430 | if dependency.projects.hasKey(directory): 431 | raise newException(Defect, "attempt to duplicate project dependency") 432 | dependency.projects.add directory, project 433 | dependency.addName project.name 434 | # this'll help anyone sniffing around thinking packages precede projects 435 | dependency.add project.asPackage 436 | 437 | proc newDependency*(project: Project): Dependency = 438 | ## convenience to form a new dependency on a specific project 439 | let 440 | requirement = newRequirement(project.name, Equal, project.release) 441 | requirement.notes = project.name 442 | result = newDependency(requirement) 443 | result.add project.repo, project 444 | 445 | proc mergeContents(existing: var Dependency; dependency: Dependency): bool = 446 | ## combine two dependencies and yield true if a new project is added 447 | # add the requirement as a child of the existing requirement 448 | existing.requirement.adopt dependency.requirement 449 | # adding the packages as a group will work 450 | existing.add dependency.packages 451 | # add projects according to their repo 452 | for directory, project in dependency.projects.pairs: 453 | if directory in existing.projects: 454 | continue 455 | existing.projects.add directory, project 456 | result = true 457 | 458 | proc addName(group: var DependencyGroup; req: Requirement; dep: Dependency) = 459 | ## add any import names from the dependency into the dependency group 460 | for directory, project in dep.projects.pairs: 461 | let name = project.importName 462 | if name notin group.imports: 463 | group.imports[name] = directory 464 | elif group.imports[name] != directory: 465 | warn &"name collision for import `{name}`:" 466 | for path in [directory, group.imports[name]]: 467 | warn &"\t{path}" 468 | when defined(debugImportNames): 469 | when not defined(release) and not defined(danger): 470 | for name in dep.names.items: 471 | if not group.imports.hasKey(name): 472 | warn &"{name} was in {dep.names} but not group names:" 473 | warn $group.imports 474 | assert group.imports.hasKey(name) 475 | 476 | proc add*(group: var DependencyGroup; req: Requirement; dep: Dependency) = 477 | group.table.add req, dep 478 | group.addName req, dep 479 | 480 | proc addedRequirements*(dependencies: var DependencyGroup; 481 | dependency: var Dependency): bool = 482 | ## add a dependency to the group and return true if the 483 | ## addition added new requirements to the group 484 | let 485 | required = dependency.requirement 486 | var 487 | existing: Dependency 488 | 489 | block complete: 490 | 491 | # we're looking for an existing dependency to merge into 492 | block found: 493 | 494 | # check to see if an existing project will work 495 | for req, dep in dependencies.mpairs: 496 | for directory, project in dep.projects.mpairs: 497 | if required.isSatisfiedBy(project): 498 | existing = dep 499 | break found 500 | 501 | # failing that, check to see if an existing package matches 502 | for req, dep in dependencies.mpairs: 503 | for url, package in dep.packages.pairs: 504 | if package.url in dependency.packages: 505 | existing = dep 506 | break found 507 | 508 | # as a last ditch effort which will be used for lockfile/unlock, 509 | # try to match against an identity in the requirements exactly 510 | for req, dep in dependencies.mpairs: 511 | for child in req.orphans: 512 | if child.identity == required.identity: 513 | existing = dep 514 | break found 515 | 516 | # found nothing; install the dependency in the group 517 | dependencies.add required, dependency 518 | # we've added requirements we can analyze only if projects exist 519 | result = dependency.projects.len > 0 520 | break complete 521 | 522 | # if we found a good merge target, then merge our existing dependency 523 | result = existing.mergeContents dependency 524 | # point to the merged dependency 525 | dependency = existing 526 | 527 | proc pathForName*(dependencies: DependencyGroup; name: string): Option[string] = 528 | ## try to retrieve the directory for a given import 529 | if dependencies.imports.hasKey(name): 530 | result = dependencies.imports[name].some 531 | 532 | proc projectForPath*(deps: DependencyGroup; path: string): Option[Project] = 533 | ## retrieve a project from the dependencies using its path 534 | for dependency in deps.values: 535 | if dependency.projects.hasKey(path): 536 | result = dependency.projects[path].some 537 | break 538 | 539 | proc reqForProject*(group: DependencyGroup; project: Project): Option[Requirement] = 540 | ## try to retrieve a requirement given a project 541 | for requirement, dependency in group.pairs: 542 | if project in dependency.projects: 543 | result = requirement.some 544 | break 545 | 546 | proc projectForName*(group: DependencyGroup; name: string): Option[Project] = 547 | ## try to retrieve a project given an import name 548 | let 549 | path = group.pathForName(name) 550 | if path.isSome: 551 | result = group.projectForPath(path.get) 552 | 553 | proc isHappy*(dependency: Dependency): bool = 554 | ## true if the dependency is being met successfully 555 | result = dependency.projects.len > 0 556 | 557 | proc isHappyWithVersion*(dependency: Dependency): bool = 558 | ## true if the dependency is happy with the version of the project 559 | for project in dependency.projects.values: 560 | result = dependency.requirement.isSatisfiedBy(project, project.release) 561 | if result: 562 | break 563 | 564 | proc resolveUsing*(projects: ProjectGroup; packages: PackageGroup; 565 | requirement: Requirement): Dependency = 566 | ## filter all we know about the environment, a requirement, and the 567 | ## means by which we may satisfy it, into a single object 568 | result = newDependency(requirement) 569 | block success: 570 | 571 | # 1. is it a directory? 572 | for directory, available in projects.pairs: 573 | if not requirement.isSatisfiedBy(available): 574 | continue 575 | debug &"{available} satisfies {requirement}" 576 | result.add directory, available 577 | 578 | # seems like we found some viable deps info locally 579 | if result.isHappy: 580 | break success 581 | 582 | # 2. is it in packages? 583 | let matches = packages.matching(requirement) 584 | result.add(matches) 585 | if matches.len > 0: 586 | break success 587 | 588 | # 3. all we have is a url 589 | if requirement.isUrl: 590 | let findurl = requirement.toUrl(packages) 591 | if findurl.isSome: 592 | # if it's a url but we couldn't match it, add it to the result anyway 593 | result.add findurl.get 594 | break success 595 | 596 | proc isUsing*(dependencies: DependencyGroup; target: Target; 597 | outside: Dependency = nil): bool = 598 | ## true if the target points to a repo we're importing 599 | block found: 600 | for requirement, dependency in dependencies.pairs: 601 | if dependency == outside: 602 | continue 603 | for directory, project in dependency.projects.pairs: 604 | if directory == target.repo: 605 | result = true 606 | break found 607 | when defined(debug): 608 | debug &"is using {target.repo}: {result}" 609 | 610 | proc resolve*(project: Project; deps: var DependencyGroup; 611 | req: Requirement): bool 612 | 613 | proc resolve*(project: var Project; dependencies: var DependencyGroup): bool = 614 | ## resolve a project's dependencies recursively; store result in dependencies 615 | 616 | # assert a usable config 617 | assert project.cfg != nil 618 | 619 | if Flag.Quiet notin dependencies.flags: 620 | info &"{project.cuteRelease:>8} {project.name:>12} {project.releaseSummary}" 621 | 622 | # assume innocence until the guilt is staining the carpet in the den 623 | result = true 624 | 625 | block complete: 626 | # start with determining the dependencies of the project 627 | let requires = project.determineDeps 628 | if requires.isNone: 629 | warn &"no requirements found for {project}" 630 | break complete 631 | 632 | # next, iterate over each requirement 633 | for requirement in requires.get.values: 634 | # and if it's not "virtual" (ie. the compiler) 635 | if requirement.isVirtual: 636 | continue 637 | # and we haven't already processed the same exact requirement 638 | if dependencies.table.hasKey(requirement): 639 | continue 640 | # then try to stay truthy while resolving that requirement, too 641 | result = result and project.resolve(dependencies, requirement) 642 | # if we failed, there's no point in continuing 643 | if not result: 644 | break complete 645 | 646 | proc resolve*(project: Project; deps: var DependencyGroup; 647 | req: Requirement): bool = 648 | ## resolve a single project's requirement, storing the result 649 | var resolved = resolveUsing(deps.projects, deps.packages, req) 650 | case resolved.packages.len: 651 | of 0: 652 | warn &"unable to resolve requirement `{req}`" 653 | result = false 654 | return 655 | of 1: 656 | discard 657 | else: 658 | project.reportMultipleResolutions(req, resolved.packages) 659 | 660 | # this game is now ours to lose 661 | result = true 662 | 663 | block complete: 664 | # if the addition of the dependency is not novel, we're done 665 | if not deps.addedRequirements(resolved): 666 | break complete 667 | 668 | # else, we'll resolve dependencies introduced in any new dependencies. 669 | # note: we're using project.cfg and project.repo as a kind of scope 670 | for recurse in resolved.projects.asFoundVia(project.cfg, project.repo): 671 | # if one of the existing dependencies is using the same project, then 672 | # we won't bother to recurse into it and process its requirements 673 | if deps.isUsing(recurse.nimble, outside = resolved): 674 | continue 675 | result = result and recurse.resolve(deps) 676 | # if we failed, there's no point in continuing 677 | if not result: 678 | break complete 679 | 680 | proc getOfficialPackages(project: Project): PackagesResult = 681 | result = getOfficialPackages(project.nimbleDir) 682 | 683 | proc newDependencyGroup*(project: Project; 684 | flags = defaultFlags): DependencyGroup = 685 | ## a convenience to load packages and projects for resolution 686 | result = newDependencyGroup(flags) 687 | 688 | # try to load the official packages list; either way, a group will exist 689 | let official = project.getOfficialPackages 690 | result.packages = official.packages 691 | 692 | # collect all the packages from the environment 693 | result.projects = project.childProjects 694 | 695 | proc reset*(dependencies: var DependencyGroup; project: var Project) = 696 | ## reset a dependency group and prepare to resolve dependencies again 697 | # empty the group of all requirements and dependencies 698 | dependencies.clear 699 | # reset the project's configuration to find new paths, etc. 700 | project.cfg = loadAllCfgs(project.repo) 701 | # rescan for package dependencies applicable to this project 702 | dependencies.projects = project.childProjects 703 | 704 | proc roll*(project: var Project; requirement: Requirement; 705 | goal: RollGoal; dry_run = false): bool = 706 | ## true if the project meets the requirement and goal 707 | if project.dist != Git: 708 | return 709 | if project.tags == nil: 710 | project.fetchTagTable 711 | let 712 | current = project.version 713 | 714 | head := project.getHeadOid: 715 | # no head means that we're up-to-date, obviously 716 | return 717 | 718 | # up-to-date until proven otherwise 719 | result = true 720 | 721 | # get the list of suitable releases as a seq... 722 | var 723 | releases = toSeq project.symbolicMatch(requirement) 724 | case goal: 725 | of Upgrade: 726 | # ...so we can reverse it if needed to invert semantics 727 | releases.reverse 728 | of Downgrade: 729 | discard 730 | of Specific: 731 | raise newException(Defect, "not implemented") 732 | 733 | # iterate over all matching releases in order 734 | for index, match in releases.pairs: 735 | # we may one day support arbitrary rolls 736 | if match.kind != Tag: 737 | debug &"dunno how to roll to {match}" 738 | continue 739 | 740 | # if we're at the next best release then we're done 741 | if match.kind == Tag and match.reference == $head: 742 | break 743 | 744 | # make a friendly name for the future version 745 | let 746 | friendly = block: 747 | if match.kind in {Tag}: 748 | project.tags.shortestTag(match.reference) 749 | else: 750 | $match 751 | if dry_run: 752 | # make some noise and don't actually do anything 753 | info &"would {goal} {project.name} from {current} to {friendly}" 754 | result = false 755 | break 756 | 757 | # make sure we don't do something stupid 758 | if not project.repoLockReady: 759 | error &"refusing to roll {project.name} 'cause it's dirty" 760 | result = false 761 | break 762 | 763 | # try to point to the matching release 764 | result = project.setHeadToRelease(match) 765 | if not result: 766 | warn &"failed checkout of {match}" 767 | continue 768 | else: 769 | notice &"rolled {project.name} from {current} to {friendly}" 770 | # freshen project version, release, etc. 771 | project.refresh 772 | # then, maybe rename the directory appropriately 773 | if project.parent != nil: 774 | project.parent.relocateDependency(project) 775 | result = true 776 | break 777 | 778 | proc rollTowards*(project: var Project; requirement: Requirement): bool = 779 | ## advance the head of a project to meet a given requirement 780 | if project.dist != Git: 781 | return 782 | if project.tags == nil: 783 | project.fetchTagTable 784 | 785 | # reverse the order of matching releases so that we start with the latest 786 | # valid release first and proceed to lesser versions thereafter 787 | var releases = toSeq project.symbolicMatch(requirement) 788 | releases.reverse 789 | 790 | # iterate over all matching tags 791 | for match in releases.items: 792 | # try to point to the matching release 793 | result = project.setHeadToRelease(match) 794 | if not result: 795 | warn &"failed checkout of {match}" 796 | continue 797 | # freshen project version, release, etc. 798 | project.refresh 799 | # then, maybe rename the directory appropriately 800 | if project.parent != nil: 801 | project.parent.relocateDependency(project) 802 | # critically, end after a successful roll 803 | break 804 | --------------------------------------------------------------------------------