├── nim.cfg ├── tools ├── helloworld.nim └── fetcher.nim ├── src ├── nimby.nims └── nimby.nim ├── docs └── nimbyLogo.png ├── .gitignore ├── nimby.nimble ├── .github └── workflows │ ├── nimble_speed.yml │ ├── test.yml │ ├── nimby_speed.yml │ ├── test_install_from_file.yml │ ├── test_sync_lock_file.yml │ ├── test_install_nim.yml │ ├── test_install_nim_source.yml │ ├── release.yml │ └── test_from_nothing.yml ├── tests ├── test_commands.nim └── test_parsing.nim ├── LICENSE ├── AGENTS.md └── README.md /nim.cfg: -------------------------------------------------------------------------------- 1 | --d:ssl 2 | -------------------------------------------------------------------------------- /tools/helloworld.nim: -------------------------------------------------------------------------------- 1 | echo "Hello, World!" -------------------------------------------------------------------------------- /src/nimby.nims: -------------------------------------------------------------------------------- 1 | --threads:on 2 | --mm:atomicArc 3 | -------------------------------------------------------------------------------- /docs/nimbyLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/nimby/HEAD/docs/nimbyLogo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore files with no extention: 2 | * 3 | !*/ 4 | !*.* 5 | 6 | # normal ignores: 7 | *.exe 8 | nimcache 9 | packages 10 | -------------------------------------------------------------------------------- /nimby.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.1.13" 4 | author = "Andre von Houck" 5 | description = "Nimby helps you manage many nim packages." 6 | license = "MIT" 7 | srcDir = "src" 8 | 9 | # Dependencies 10 | 11 | requires "nim >= 2.0.0" 12 | 13 | bin = @["nimby"] 14 | -------------------------------------------------------------------------------- /.github/workflows/nimble_speed.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Nimble Speed Test 3 | on: 4 | workflow_dispatch: 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | os: [ubuntu-latest] 10 | runs-on: ${{ matrix.os }} 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: treeform/setup-nim-action@v2 14 | 15 | - name: Test Speed 16 | run: nimble install fidget2 17 | 18 | - name: List Installed Nim packages 19 | run: nimble list -i --ver 20 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | jobs: 8 | test: 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest] 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: treeform/setup-nim-action@v3 16 | 17 | - name: Build Nimby 18 | run: nim c -d:release -d:monkey src/nimby.nim 19 | 20 | - name: Add Nimby to PATH 21 | run: echo "$PWD/src" >> "$GITHUB_PATH" 22 | 23 | - name: Run Nimby Tests 24 | run: nim r tests/test_parsing.nim 25 | -------------------------------------------------------------------------------- /.github/workflows/nimby_speed.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Nimby Speed Test 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | jobs: 9 | build: 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest] 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: treeform/setup-nim-action@v3 17 | 18 | - name: Compile Nimby 19 | run: nim c -d:release src/nimby.nim 20 | 21 | - name: Test Speed 22 | run: src/nimby install fidget2 23 | 24 | - name: List Installed Nim packages 25 | run: src/nimby list 26 | -------------------------------------------------------------------------------- /.github/workflows/test_install_from_file.yml: -------------------------------------------------------------------------------- 1 | name: Test Install From a .nimble File 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | jobs: 8 | build: 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest, macos-latest, windows-latest] 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: treeform/setup-nim-action@v3 16 | 17 | - name: Compile Nimby 18 | run: nim c -d:release -d:monkey src/nimby.nim 19 | 20 | - name: Boxy 21 | run: git clone --depth 1 https://github.com/treeform/boxy.git 22 | 23 | - name: Sync Metta Lock File 24 | run: src/nimby install boxy/boxy.nimble 25 | 26 | - name: List Installed Packages 27 | run: src/nimby list 28 | 29 | - name: List Dependency Tree 30 | run: src/nimby tree mettagrid/nim/mettascope 31 | 32 | - name: Build Boxy 33 | run: nim c -d:release boxy/src/boxy.nim 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/test_commands.nim: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | proc cmd(command: string) = 5 | echo "> ", command 6 | let result = execShellCmd(command) 7 | if result != 0: 8 | raise newException(Exception, "Command failed: " & $result) 9 | 10 | # cmd("nimby --help") 11 | # cmd("nimby -h") 12 | # cmd("nimby --version") 13 | # cmd("nimby -v") 14 | # cmd("nimby --help") 15 | # cmd("nimby -h") 16 | # cmd("nimby --version") 17 | # cmd("nimby -v") 18 | 19 | removeDir(expandTilde("~/.nimby/pkgs")) 20 | removeDir(expandTilde("~/.nimby/tmp")) 21 | createDir(expandTilde("~/.nimby/tmp")) 22 | setCurrentDir(expandTilde("~/.nimby/tmp")) 23 | 24 | cmd("nimby install -V mummy") 25 | doAssert dirExists("mummy") 26 | cmd("nimby remove mummy") 27 | 28 | removeDir(expandTilde("~/.nimby/pkgs")) 29 | removeDir(expandTilde("~/.nimby/tmp")) 30 | createDir(expandTilde("~/.nimby/tmp")) 31 | setCurrentDir(expandTilde("~/.nimby/tmp")) 32 | 33 | cmd("nimby install -g -V mummy") 34 | doAssert not dirExists("mummy") 35 | doAssert dirExists(expandTilde("~/.nimby/pkgs/mummy")) 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT 2 | 3 | Copyright 2018 Andre von Houck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /.github/workflows/test_sync_lock_file.yml: -------------------------------------------------------------------------------- 1 | name: Test Sync Lock File 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | jobs: 8 | build: 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest, macos-latest, windows-latest] 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: treeform/setup-nim-action@v3 16 | 17 | - name: Compile Nimby 18 | run: nim c -d:release -d:monkey src/nimby.nim 19 | 20 | - name: Clone Metta (with MettaScope) 21 | run: git clone --depth 1 https://github.com/Metta-AI/metta.git 22 | 23 | - name: Sync Metta Lock File 24 | run: src/nimby sync metta/packages/mettagrid/nim/mettascope/nimby.lock 25 | 26 | - name: List Installed Packages 27 | run: src/nimby list 28 | 29 | - name: List Dependency Tree 30 | run: src/nimby tree mettagrid/nim/mettascope 31 | 32 | - name: Build MettaScope 33 | run: nim c -d:release metta/packages/mettagrid/nim/mettascope/src/mettascope.nim 34 | 35 | 36 | -------------------------------------------------------------------------------- /.github/workflows/test_install_nim.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Test install Nim 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | jobs: 9 | build: 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest] 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: treeform/setup-nim-action@v3 17 | 18 | - name: Compile Nimby 19 | run: nim c -d:release -d:monkey src/nimby.nim 20 | 21 | - name: Install Nim Compiler 22 | run: src/nimby use -V 2.2.6 23 | 24 | - name: Print Nimby Folder 25 | run: ls "$HOME/.nimby" 26 | 27 | - name: Print Nimby nim folder 28 | run: ls "$HOME/.nimby/nim" 29 | 30 | - name: Print Nimby nim/bin folder 31 | run: ls "$HOME/.nimby/nim/bin" 32 | 33 | - name: Add Nim to PATH 34 | run: echo "$HOME/.nimby/nim/bin" >> "$GITHUB_PATH" 35 | 36 | - name: Print PATH 37 | run: echo "$PATH" 38 | 39 | - name: Print Nim version (Full Path) 40 | run: $HOME/.nimby/nim/bin/nim --version 41 | 42 | - name: Print Nim version (Normal) 43 | run: nim --version 44 | 45 | - name: Print Which Nim 46 | run: which nim 47 | 48 | - name: List the Nim folder 49 | shell: bash 50 | run: ls -la "$HOME/.nimby/nim" 51 | 52 | - name: Compile & Run Hello World 53 | run: nim c -r tools/helloworld.nim 54 | 55 | -------------------------------------------------------------------------------- /.github/workflows/test_install_nim_source.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Test install Nim form Source 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | jobs: 9 | build: 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, windows-latest, macos-latest] 13 | fail-fast: false 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: treeform/setup-nim-action@v3 18 | 19 | - name: Compile Nimby 20 | shell: bash 21 | run: nim c -d:release src/nimby.nim 22 | 23 | - name: Install Nim Compiler 24 | shell: bash 25 | run: src/nimby use --source --verbose 2.2.4 26 | 27 | - name: Print Nimby Folder 28 | shell: bash 29 | run: ls "$HOME/.nimby" 30 | 31 | - name: Print Nimby nim folder 32 | shell: bash 33 | run: ls "$HOME/.nimby/nim" 34 | 35 | - name: Print Nimby nim/bin folder 36 | shell: bash 37 | run: ls "$HOME/.nimby/nim/bin" 38 | 39 | - name: Add Nim to PATH 40 | shell: bash 41 | run: echo "$HOME/.nimby/nim/bin" >> "$GITHUB_PATH" 42 | 43 | - name: Print PATH 44 | shell: bash 45 | run: echo "$PATH" 46 | 47 | - name: Print Nim version (Full Path) 48 | shell: bash 49 | run: $HOME/.nimby/nim/bin/nim --version 50 | 51 | - name: Print Nim version (Normal) 52 | shell: bash 53 | run: nim --version 54 | 55 | - name: Print Which Nim 56 | shell: bash 57 | run: which nim 58 | 59 | - name: List the Nim folder 60 | shell: bash 61 | run: ls -la "$HOME/.nimby/nim" 62 | 63 | - name: Compile & Run Hello World 64 | run: nim c -r tools/helloworld.nim 65 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Nimby Release Binaries 2 | 3 | on: 4 | release: 5 | types: [published] 6 | push: 7 | tags: 8 | - "*" 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: write 13 | 14 | jobs: 15 | build: 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, macos-latest, windows-latest] 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v3 22 | - uses: treeform/setup-nim-action@v3 23 | 24 | - name: Compute artifact name 25 | id: meta 26 | shell: bash 27 | run: | 28 | if [ "${{ runner.os }}" = "Windows" ]; then ext=".exe"; else ext=""; fi 29 | name="nimby-${{ runner.os }}-${{ runner.arch }}${ext}" 30 | echo "artifact=${name}" >> "$GITHUB_OUTPUT" 31 | 32 | - name: Build nimby 33 | shell: bash 34 | run: | 35 | mkdir -p dist 36 | nim c -d:release -o:dist/${{ steps.meta.outputs.artifact }} src/nimby.nim 37 | 38 | - name: Upload artifact 39 | uses: actions/upload-artifact@v4 40 | with: 41 | name: ${{ steps.meta.outputs.artifact }} 42 | path: dist/${{ steps.meta.outputs.artifact }} 43 | 44 | release: 45 | needs: build 46 | if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'release' 47 | runs-on: ubuntu-latest 48 | steps: 49 | - name: Download build artifacts 50 | uses: actions/download-artifact@v4 51 | with: 52 | path: dist 53 | merge-multiple: true 54 | 55 | - name: List artifacts 56 | run: ls -lah dist 57 | 58 | - name: Upload assets to GitHub Release 59 | uses: softprops/action-gh-release@v1 60 | with: 61 | files: dist/* 62 | env: 63 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 64 | -------------------------------------------------------------------------------- /tests/test_parsing.nim: -------------------------------------------------------------------------------- 1 | import 2 | std/[os], 3 | ../src/nimby 4 | 5 | echo "Test 1: parses version and srcDir without deps" 6 | let path1 = getTempDir() / "test1.nimble" 7 | writeFile(path1, """ 8 | version = "0.1.2" 9 | srcDir = "src" 10 | """) 11 | let n1 = parseNimbleFile(path1) 12 | doAssert n1.version == "0.1.2", "version should parse" 13 | doAssert n1.srcDir == "src", "srcDir should parse" 14 | doAssert n1.dependencies.len == 0, "no deps expected" 15 | removeFile(path1) 16 | 17 | echo "Test 2: parses nim version requirement as a dep" 18 | let path2 = getTempDir() / "test2.nimble" 19 | writeFile(path2, """ 20 | version = "1.2.3" 21 | srcDir = "lib" 22 | requires "nim >= 1.6.2" 23 | """) 24 | let n2 = parseNimbleFile(path2) 25 | doAssert n2.version == "1.2.3" 26 | doAssert n2.srcDir == "lib" 27 | doAssert n2.nimDependency == Dependency(name: "nim", op: ">=", version: "1.6.2") 28 | doAssert n2.dependencies.len == 0 29 | removeFile(path2) 30 | 31 | echo "Test 3: parses no formula for dependency" 32 | let path3 = getTempDir() / "test3.nimble" 33 | writeFile(path3, """ 34 | version = "0.0.1" 35 | srcDir = "src" 36 | requires "pixie" 37 | """) 38 | let n3 = parseNimbleFile(path3) 39 | doAssert n3.dependencies.len == 1 40 | doAssert n3.dependencies[0] == Dependency(name: "pixie", op: "", version: "") 41 | removeFile(path3) 42 | 43 | echo "Test 4: parses multiple requires lines and preserves order" 44 | let path4 = getTempDir() / "test4.nimble" 45 | writeFile(path4, """ 46 | version = "2.0.0" 47 | srcDir = "src" 48 | requires "pixie >= 0.3.1" 49 | requires "chroma == 0.2.0" 50 | """) 51 | let n4 = parseNimbleFile(path4) 52 | doAssert n4.dependencies.len == 2 53 | doAssert n4.dependencies[0] == Dependency(name: "pixie", op: ">=", version: "0.3.1") 54 | doAssert n4.dependencies[1] == Dependency(name: "chroma", op: "==", version: "0.2.0") 55 | removeFile(path4) 56 | 57 | echo "Test 5: tolerates extra whitespace in requires line" 58 | let path5 = getTempDir() / "test5.nimble" 59 | writeFile(path5, """ 60 | version = "3.0.0a" 61 | srcDir = "src" 62 | requires "vmath >= 1.0.0" 63 | """) 64 | let n5 = parseNimbleFile(path5) 65 | doAssert n5.version == "3.0.0a", "version should parse" 66 | doAssert n5.srcDir == "src", "srcDir should parse" 67 | doAssert n5.dependencies.len == 1 68 | doAssert n5.dependencies[0] == Dependency(name: "vmath", op: ">=", version: "1.0.0") 69 | removeFile(path5) 70 | 71 | echo "All parseNimbleFile tests passed." 72 | -------------------------------------------------------------------------------- /.github/workflows/test_from_nothing.yml: -------------------------------------------------------------------------------- 1 | name: Test setup Nim from nothing. 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | jobs: 8 | build: 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest, windows-latest, macos-latest] 12 | runs-on: ${{ matrix.os }} 13 | env: 14 | NIM_VERSION: '2.2.6' 15 | NIMBY_VERSION: '0.1.13' 16 | steps: 17 | 18 | - name: Setup Nim via Nimby (Windows) 19 | if: runner.os == 'Windows' 20 | shell: bash 21 | run: | 22 | set -e 23 | echo "Downloading Nimby for Windows..." 24 | curl -sSL -o nimby.exe "https://github.com/treeform/nimby/releases/download/$NIMBY_VERSION/nimby-Windows-X64.exe" 25 | chmod +x nimby.exe 26 | echo "Using Nimby to install Nim $NIM_VERSION..." 27 | ./nimby.exe use "$NIM_VERSION" 28 | cp nimby.exe $HOME/.nimby/nim/bin/nimby.exe 29 | echo "Adding Nim bin to PATH..." 30 | echo "$(cygpath -w "$HOME")\.nimby\nim\bin" >> "$GITHUB_PATH" 31 | 32 | - name: Setup Nim via Nimby (macOS) 33 | if: runner.os == 'macOS' 34 | shell: bash 35 | run: | 36 | set -e 37 | arch="$(uname -m)" 38 | if [ "$arch" = "arm64" ]; then 39 | NIMBY_URL="https://github.com/treeform/nimby/releases/download/$NIMBY_VERSION/nimby-macOS-ARM64" 40 | else 41 | NIMBY_URL="https://github.com/treeform/nimby/releases/download/$NIMBY_VERSION/nimby-macOS-X64" 42 | fi 43 | echo "Downloading Nimby for macOS ($arch)..." 44 | curl -sSL -o nimby "$NIMBY_URL" 45 | chmod +x nimby 46 | echo "Using Nimby to install Nim $NIM_VERSION..." 47 | ./nimby use "$NIM_VERSION" 48 | cp nimby $HOME/.nimby/nim/bin/nimby 49 | chmod +x $HOME/.nimby/nim/bin/nimby 50 | echo "Adding Nim bin to PATH..." 51 | echo "$HOME/.nimby/nim/bin" >> "$GITHUB_PATH" 52 | 53 | - name: Setup Nim via Nimby (Linux) 54 | if: runner.os == 'Linux' 55 | shell: bash 56 | run: | 57 | set -e 58 | echo "Downloading Nimby for Linux..." 59 | curl -sSL -o nimby "https://github.com/treeform/nimby/releases/download/$NIMBY_VERSION/nimby-Linux-X64" 60 | chmod +x nimby 61 | echo "Using Nimby to install Nim $NIM_VERSION..." 62 | ./nimby use "$NIM_VERSION" 63 | cp nimby $HOME/.nimby/nim/bin/nimby 64 | chmod +x $HOME/.nimby/nim/bin/nimby 65 | echo "Adding Nim bin to PATH..." 66 | echo "$HOME/.nimby/nim/bin" >> "$GITHUB_PATH" 67 | 68 | - name: Install Fidget2 69 | run: nimby install fidget2 70 | 71 | - name: List Installed Nim packages 72 | run: nimby list 73 | 74 | - name: List Dependencies in Tree View 75 | run: nimby tree fidget2 76 | 77 | - name: Test Fidget2 Compilation 78 | run: nim c -r fidget2/src/fidget2.nim 79 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # Nim coding guidelines for AI and maybe humans. 2 | 3 | ## Abstractions 4 | 5 | Please follow Handmade manifesto ideas of minimal abstraction, simple data structures, and linear straightforward code. 6 | If function is called only one time, just inline it unless it's deeply nested. 7 | Use proper meta programming for the right things. 8 | * Try simple types. 9 | * Only when types are not enough, try generics. 10 | * Only when generics are not enough, try templates. 11 | * Only when templates are not enough, try macros. 12 | 13 | ## Anatomy of a Nim file 14 | 15 | Here is the anatomy of a Nim file: 16 | * Imports 17 | * Constants 18 | * Types 19 | * Variables 20 | * Procedures 21 | * when isMainModule (use rarely, never for tests) 22 | 23 | ## Imports 24 | 25 | Imports should start with std modules then external modules, then local modules. Ideally in 3 lines like this: 26 | ``` 27 | import 28 | std/[os, random, strutils], 29 | fidget2, boxy, windy, 30 | common, internal, models, widgets. 31 | ``` 32 | 33 | Use plural for modules unless it's common.nim. 34 | If a module deals with Player, use `players.nim`. 35 | Always try to use single English words for module names. 36 | Some modules will have `test_` or `bench_` prefix. 37 | 38 | ## Tests 39 | 40 | Don't use unit test framework, use doAssert and echos instead. 41 | Testing is hard and they should be as simple, almost stupid simple. 42 | Use a single tests/tests.nim file for all tests. 43 | 44 | ```nim 45 | echo "Testing equality" 46 | doAssert a == b, "a should be equal to b" 47 | ``` 48 | 49 | If it gets too big, split it into multiple files all starting with test_. 50 | 51 | After testing, benchmarking is just as important. 52 | Also write bench_*.nim files for benchmarks using benchy library. 53 | 54 | ```nim 55 | import benchy, std/os, std/random 56 | 57 | timeIt "number counter": 58 | var s = 0 59 | for i in 0 .. 1_000_000: 60 | s += s 61 | ``` 62 | 63 | ## Names 64 | 65 | Best names are single English words. Only go to two or three words if absolutely necessary. 66 | Use common abbreviations like HTTP, API, JSON, etc. 67 | Use camelCase for variables and functions. 68 | Use PascalCase for types, constants, and enums. 69 | Use plural for arrays and maps and other collections. 70 | When iterating and only when using integers prefer to use `i`, `j`, `k` etc... 71 | 72 | ## Variables 73 | 74 | At the top level prefer to use `const` over `let`. Note: in Nim const use CamelCase with capital first letter. 75 | Prefer to use `let` over `var` unless you need to mutate the variable. 76 | Merge multiple const, let, and var declarations into a single block declaration. 77 | 78 | ## Readme 79 | 80 | Fix spelling and grammar, only! 81 | Avoid using emoji in the readme, avoid using fancy quotes, mdash, semicolon, and other fancy characters. Write in a simple, clear, and direct way. Bullet lists or table to show features are good. 82 | 83 | ## Indentation 84 | 85 | Use 2 spaces for indentation. 86 | Never use double lines even between types, procs or sections. 87 | If breaking a large function call break it into a line per argument. 88 | 89 | ```nim 90 | func( 91 | arg1, 92 | arg2, 93 | arg3 94 | ) 95 | ``` 96 | 97 | If body of a if or loop is too large, break it into a line per statement, but then indent the body by 4 spaces. 98 | 99 | ```nim 100 | if condition or 101 | longCondition or 102 | anotherLongCondition: 103 | statement1 104 | statement2 105 | statement3 106 | ``` 107 | 108 | Don't indent the body of a case statement. Prefer to use enums and case statements together. 109 | 110 | ```nim 111 | case expression: 112 | of value1: 113 | statement1 114 | of value2: 115 | statement2 116 | else: 117 | statement4 118 | ``` 119 | 120 | ## Comments 121 | 122 | Have all comments be complete sentences. 123 | Start with a capital letter and end with a period. 124 | Make sure all functions have doc comments. 125 | Try to only use a single line per doc comment. 126 | Never more than 4 lines. 127 | Avoid top level section comments, especially surround with `=` or `#` characters. 128 | 129 | ## Error Handling 130 | 131 | Its best to let the exception propagate to the top level. Don't silence them with `try/except`. Use error codes where absolutely necessary. Adding asserts especially at start or end of a procedure is good as they can be compiled out in release mode. Many errors are not actually errors and can be passed through. Prefer returning nil, "", 0, false, over raising exceptions or error codes. 132 | 133 | ## Checking the code 134 | 135 | In many projects you can run `nim check` and `nimble test` as you are writing the code to make sure it works. Always do this after big changes and before committing. 136 | 137 | ## Block formatting 138 | 139 | Some small or repeating functions its ok to be without a doc comment. 140 | 141 | ```nim 142 | proc toFlatty*(s: var string, x: uint8) = s.addUint8(x) 143 | proc toFlatty*(s: var string, x: int8) = s.addInt8(x) 144 | proc toFlatty*(s: var string, x: uint16) = s.addUint16(x) 145 | proc toFlatty*(s: var string, x: int16) = s.addInt16(x) 146 | proc toFlatty*(s: var string, x: uint32) = s.addUint32(x) 147 | proc toFlatty*(s: var string, x: int32) = s.addInt32(x) 148 | proc toFlatty*(s: var string, x: uint64) = s.addUint64(x) 149 | proc toFlatty*(s: var string, x: int64) = s.addInt64(x) 150 | proc toFlatty*(s: var string, x: float32) = s.addFloat32(x) 151 | proc toFlatty*(s: var string, x: float64) = s.addFloat64(x) 152 | ``` 153 | 154 | Some functions that are just use to interface with external libraries its ok to be without a doc comment and be on long lines. 155 | 156 | ```nim 157 | proc WinHttpReceiveResponse*(hRequest: HINTERNET, lpReserved: LPVOID): BOOL {.dynlib: "winhttp".} 158 | proc WinHttpQueryHeaders*(hRequest: HINTERNET, dwInfoLevel: DWORD, pwszName: LPCWSTR, lpBuffer: LPVOID, lpdwBufferLength: LPDWORD, lpdwIndex: LPDWORD): BOOL {.dynlib: "winhttp".} 159 | proc WinHttpReadData*(hFile: HINTERNET, lpBuffer: LPVOID, dwNumberOfBytesToRead: DWORD, lpdwNumberOfBytesRead: LPDWORD): BOOL {.dynlib: "winhttp".} 160 | ``` 161 | 162 | Its ok to add zero to make blocks line up: 163 | 164 | ``` 165 | for i in 0 ..< 10: 166 | echo data[i * 3 + 0] 167 | echo data[i * 3 + 1] 168 | echo data[i * 3 + 2] 169 | ``` 170 | 171 | -------------------------------------------------------------------------------- /tools/fetcher.nim: -------------------------------------------------------------------------------- 1 | import 2 | std/[strformat, strutils, os, json, base64, times], 3 | curly, jsony 4 | 5 | let githubToken = block: 6 | let tokenPath = expandTilde("~/.github_token") 7 | if fileExists(tokenPath): 8 | readFile(tokenPath).strip() 9 | else: 10 | quit("GitHub token not found") 11 | 12 | let curl = newCurly() # Best to start with a single long-lived instance 13 | 14 | proc githubHeaders(token: string): HttpHeaders = 15 | var headers: HttpHeaders 16 | headers["Accept"] = "application/vnd.github+json" 17 | headers["X-GitHub-Api-Version"] = "2022-11-28" 18 | headers["User-Agent"] = "nimby-fetcher/1.0" 19 | if token.len > 0: 20 | headers["Authorization"] = "Bearer " & token 21 | headers 22 | 23 | proc extractQuotedStrings(line: string): seq[string] = 24 | var i = 0 25 | while i < line.len: 26 | let start = line.find('"', i) 27 | if start < 0: break 28 | let stop = line.find('"', start + 1) 29 | if stop < 0: break 30 | result.add line[(start + 1) ..< stop] 31 | i = stop + 1 32 | 33 | type RequireEntry = tuple[name: string, op: string, version: string] 34 | 35 | proc parseRequires(nimbleText: string): seq[RequireEntry] = 36 | var inArray = false 37 | for raw in nimbleText.splitLines(): 38 | let line = raw.strip() 39 | if not inArray: 40 | if line.startsWith("requires "): 41 | for q in extractQuotedStrings(line): 42 | let parts = q.splitWhitespace() 43 | if parts.len >= 3: 44 | result.add (parts[0], parts[1], parts[2].strip(chars = {'"', ',', ')'})) 45 | elif parts.len == 1: 46 | result.add (parts[0], "", "") 47 | if line.contains("@[") and not line.contains("]"): 48 | inArray = true 49 | else: 50 | for q in extractQuotedStrings(line): 51 | let parts = q.splitWhitespace() 52 | if parts.len >= 3: 53 | result.add (parts[0], parts[1], parts[2].strip(chars = {'"', ',', ')'})) 54 | elif parts.len == 1: 55 | result.add (parts[0], "", "") 56 | if line.contains("]"): 57 | inArray = false 58 | 59 | type GlobalPackage = ref object 60 | name: string 61 | url: string 62 | `method`: string 63 | tags: seq[string] 64 | description: string 65 | license: string 66 | web: string 67 | doc: string 68 | 69 | var packages: seq[GlobalPackage] 70 | 71 | proc findGlobalPackage(name: string): GlobalPackage = 72 | for package in packages: 73 | if package.name == name: 74 | return package 75 | return nil 76 | 77 | proc fetchGlobalPackages() = 78 | let url = "https://raw.githubusercontent.com/nim-lang/packages/master/packages.json" 79 | let response = curl.get(url) 80 | if response.code == 200: 81 | packages = fromJson(response.body, seq[GlobalPackage]) 82 | echo "Fetched ", packages.len, " global packages" 83 | else: 84 | echo response.code 85 | echo response.body 86 | 87 | proc fetchZip(owner: string, name: string, tag: string, indent: string = "") = 88 | if not fileExists(&"packages/{name}/{tag}/{name}.zip"): 89 | echo " Downloading: ", &"https://github.com/{owner}/{name}/archive/{tag}.zip" 90 | let response = curl.get(&"https://github.com/{owner}/{name}/archive/{tag}.zip") 91 | if response.code == 200: 92 | writeFile(&"packages/{name}/{tag}/{name}.zip", response.body) 93 | else: 94 | echo " Failed to download: ", &"https://github.com/{owner}/{name}/archive/{tag}.zip" 95 | echo " " & $response.code 96 | 97 | proc fetchNimble(owner: string, name: string, tag: string, indent: string = "") = 98 | var nimbleName = name 99 | nimbleName.removePrefix("nim-") 100 | let nimblePath = &"packages/{name}/{tag}/{nimbleName}.nimble" 101 | if fileExists(nimblePath): 102 | echo " Nimble file already exists: ", nimblePath 103 | return 104 | let url = &"https://raw.githubusercontent.com/{owner}/{name}/{tag}/{nimbleName}.nimble" 105 | let response = curl.get(url, githubHeaders(githubToken)) 106 | if response.code == 200: 107 | writeFile(nimblePath, response.body) 108 | for req in parseRequires(response.body): 109 | echo &"{indent} {req.name} {req.op} {req.version}" 110 | # let package = findGlobalPackage(req.name) 111 | # if package == nil: 112 | # echo "Package not found: ", req.name 113 | # continue 114 | # if package.`method` != "git": 115 | # echo "Package is not a git repository: ", req.name 116 | # continue 117 | # if not package.url.startsWith("https://github.com/"): 118 | # echo "Package is not a GitHub repository: ", req.name 119 | # continue 120 | # fetchNimble(owner, package.name, tag, indent & " ") 121 | # fetchZip(owner, name, tag, indent & " ") 122 | else: 123 | echo indent & " " & $response.code 124 | 125 | proc fetchTags(owner: string, package: string, indent: string = "") = 126 | let url = &"https://api.github.com/repos/{owner}/{package}/tags?per_page=100" 127 | let response = curl.get(url, githubHeaders(githubToken)) 128 | if response.code == 200: 129 | let arr = parseJson(response.body) 130 | echo &"{indent}{package} tags:" 131 | var numTags = 0 132 | for item in arr.items: 133 | let tag = item["name"].getStr 134 | echo &"{indent} {tag}" 135 | createDir(&"packages/{package}/{tag}") 136 | fetchNimble(owner, package, tag, indent & " ") 137 | inc numTags 138 | if numTags > 4: 139 | break 140 | else: 141 | echo indent & " " & $response.code 142 | 143 | createDir("packages/" & package) 144 | writeFile("packages/" & package & "/fetch_time.txt", $epochTime()) 145 | 146 | proc fetchStars(owner: string, name: string): int = 147 | if fileExists(&"packages/{name}/stars.txt"): 148 | return parseInt(readFile(&"packages/{name}/stars.txt").strip()) 149 | let url = &"https://api.github.com/repos/{owner}/{name}/stargazers?per_page=100" 150 | let response = curl.get(url, githubHeaders(githubToken)) 151 | if response.code == 200: 152 | return fromJson(response.body).len 153 | else: 154 | return 0 155 | 156 | proc fetchPackage(package: GlobalPackage, verbose: bool = false) = 157 | 158 | if verbose: echo "Fetching: ", package.url 159 | 160 | if package.`method` != "git": 161 | if verbose: echo " Package is not a git repository: ", package.name 162 | return 163 | 164 | if not package.url.startsWith("https://github.com/"): 165 | if verbose: echo " Package is not a GitHub repository: ", package.name 166 | return 167 | 168 | if package.url.contains("?"): 169 | if verbose: echo " Package has a query string: ", package.name 170 | return 171 | 172 | var 173 | arr = package.url.split("/") 174 | owner = arr[3] 175 | name = arr[4].toLowerAscii() 176 | 177 | # if name == "about": 178 | # echo " Skipping: ", package.name 179 | # echo " Invalid name?" 180 | # return 181 | 182 | createDir(&"packages/{name}") 183 | 184 | # if fileExists(&"packages/{name}/fetch_time.txt"): 185 | # let fetchTime = parseFloat(readFile(&"packages/{name}/fetch_time.txt").strip()) 186 | # if fetchTime - epochTime() < 0: 187 | # if verbose: echo " Skipping, just fetched: ", package.name 188 | # return 189 | let now = epochTime() 190 | writeFile(&"packages/{name}/fetch_time.txt", $now) 191 | 192 | let stars = fetchStars(owner, name) 193 | writeFile(&"packages/{name}/stars.txt", $stars) 194 | echo " Stars: ", stars 195 | if stars < 1: 196 | if verbose: echo " Skipping: ", package.name 197 | return 198 | 199 | echo "Fetching: ", package.url 200 | 201 | fetchTags(owner, name, " ") 202 | 203 | fetchGlobalPackages() 204 | 205 | for i, package in packages: 206 | fetchPackage(package, verbose = false) 207 | if i mod 10 == 0: 208 | echo i, "/", packages.len 209 | 210 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![Nimby Logo](docs/nimbyLogo.png) 3 | 4 | # Nimby 5 | 6 | `nimble install nimby` 7 | 8 | ![Github Actions](https://github.com/treeform/nimby/workflows/Github%20Actions/badge.svg) 9 | 10 | [API reference](https://treeform.github.io/nimby) 11 | 12 | Nimby is the fastest and simplest way to install Nim packages. 13 | It keeps things honest, transparent, and lightning fast. 14 | 15 | Instead of magic, Nimby just uses git. It clones repositories directly into your workspace, reads their `.nimble` files, and installs dependencies in parallel. Everything is shallow cloned, HEAD by design, and written straight into your `nim.cfg`. 16 | 17 | You can also install globally with `-g` in `~/.nimby/pkgs` folder. Nimby can install the Nim compiler itself as well in the `~/.nimby/nim/bin` folder. With two commands you can download Nim and install all your packages, and be ready to build in seconds around 14 seconds. 18 | 19 | --- 20 | 21 | ## Why Nimby exists 22 | 23 | When I added Nim to our company CI, our builds suddenly became very slow. Nimble installs took almost two minutes for Fidget2. That felt wrong, so I started digging. 24 | 25 | I tried replacing Nimble with a few simple shell scripts that just cloned the repos with git. It built fine, and was way way faster! 2 minutes vs 3 seconds faster. 26 | 27 | So Nimby started from a simple idea: download Nim packages from git, do not resolve dependencies, and in parallel. 28 | 29 | ## Why always install HEAD? 30 | 31 | Well, the Nim community is small, and it doesn’t really have the packaging culture that other languages do. And that’s fine. In a way, it’s actually freeing! 32 | 33 | But it also means that people rarely test older versions of packages against older versions of other packages. It's just boring thankless work after all. 34 | 35 | This makes a lot of the version numbers in requirements you see in `.nimble` files don’t really reflect reality. They might claim that a version is supported, but in practice, no one tests the old stuff. And that’s okay. It’s just how the community works. 36 | 37 | So Nimby follows the community approach and always checks out HEAD, because HEAD has the highest chance of working. Even if an API has changed, we now have AI tools that can help fix minor API changes. 38 | 39 | For development, installing from HEAD is the best way to move forward. It keeps everything current and in sync with how people actually develop Nim projects. It avoids diamond dependencies (where your package depends on A and B, but A and B depend on conflicting versions of C) and keeps things simple. I love simple things. 40 | 41 | But installing from HEAD is not good for CI, releases, or deployment to production. That’s where lock files come in. Since the community relies on HEAD, lock files give you a way to record exactly what worked at a given moment in time. 42 | 43 | Generating a lock file is easy. Commit it along with your code, and when you need to reproduce a build, Nimby can install the exact dependencies and commits listed in that file. It's just a simple text file that lists package names, URLs, and commits. 44 | 45 | So the model is simple: use HEAD (`nimby install`) for development, and use lock files for deployment (`nimby sync`). It’s the best of both worlds. 46 | It's a simple text file that lists package names, URLs, and commits. 47 | 48 | 49 | ## What is the deal with the workspace folder? 50 | 51 | You always should run `nimby` commands from the workspace folder just like you would with `git clone`. It's not wrong to think of nimby like a `git clone` with extra steps. 52 | 53 | I think the workspace folder is great. The way I have things set up, there’s a single Nim config file, and all the packages I’m working on live together as simple git checkouts. 54 | Alongside them, I also keep clones of all the dependencies I use. Everything lives in one place: 55 | 56 | ``` 57 | workspace/ 58 | nim.cfg 59 | fidget2/ 60 | pixie/ 61 | jsony/ 62 | puppy/ 63 | mummy/ 64 | .. 65 | ``` 66 | 67 | This makes it much easier to move around and explore the code base. If I’m developing something and want to see what a function does inside one of the dependencies, I can just open it right there. No hunting through hidden directories or special paths. 68 | 69 | It also helps modern AI tools. Since everything sits in one folder, they can read and understand the source code of all your dependencies at once, giving you better suggestions and context. 70 | 71 | I never liked it when packages get installed into hidden folders deep in your home directory, or when they end up scattered inside things like `deps` or `nim_modules`. It feels messy. I like everything to be clean and simple, and having all your checkouts in one visible folder is the simplest way I can think of. 72 | 73 | Not everyone develops like this, though. Sometimes you just need a tool globally and don’t want it sitting in your workspace. That’s why I added the `-g` or `--global` flag. It installs packages in a global Nimby folder `~/.nimby/pkgs` instead of the local workspace. This is especially handy for CI setups or for people who only need to use packages, not develop them. 74 | 75 | The global option works for both `nimby install -g` and even more importantly `nimby sync -g` when you’re working with lock files. That’s really all there is to it. 76 | 77 | ## What? It also installs Nim itself? 78 | 79 | Yeah, installing Nim is actually pretty easy. You just copy a couple of folders, put them in the right place, and add `~/.nimby/nim/bin` to your system path. That’s it. 80 | 81 | I think it’s a great addition to have in Nimby because it makes setup incredibly simple. You can just curl the Nimby binary for your system `curl -L -o nimby https://github.com/treeform/nimby/releases/download/v0.1.2/nimby-Linux-X64`, and that’s all you need. Then you run `./nimby use 2.2.6` with the Nim version you want, and `./nimby install your/nimby.lock` with your lock file. 82 | 83 | This works perfectly for CI workflows, deployments, or any situation where you’re starting with a blank machine. You don’t need to install anything else. Nimby downloads Nim, installs your packages, and you’re ready to go. 84 | 85 | --- 86 | 87 | ## Installation 88 | 89 | ### macOS ARM64 90 | ``` 91 | curl -L -o nimby https://github.com/treeform/nimby/releases/download/0.1.13/nimby-macOS-ARM64 92 | chmod +x nimby 93 | ``` 94 | 95 | ### Linux X64 96 | ``` 97 | curl -L -o nimby https://github.com/treeform/nimby/releases/download/0.1.13/nimby-Linux-X64 98 | chmod +x nimby 99 | ``` 100 | 101 | ### Windows 102 | ``` 103 | curl -L -o nimby.exe https://github.com/treeform/nimby/releases/download/0.1.13/nimby-Windows-X64.exe 104 | ``` 105 | 106 | 107 | --- 108 | 109 | ## Add Nim to your PATH 110 | 111 | ```sh 112 | export PATH="$HOME/.nimby/nim/bin:$PATH" 113 | ``` 114 | 115 | ```sh 116 | $env:PATH = "$HOME\.nimby\nim\bin;$env:PATH" # PowerShell 117 | ``` 118 | 119 | --- 120 | 121 | ## Quick Start 122 | 123 | Install Nim itself: 124 | 125 | ```sh 126 | nimby use 2.2.4 127 | ``` 128 | 129 | Install a package: 130 | 131 | ```sh 132 | nimby install fidget2 133 | ``` 134 | 135 | Update a package: 136 | 137 | ```sh 138 | nimby update fidget2 139 | ``` 140 | 141 | Remove a package: 142 | 143 | ```sh 144 | nimby remove fidget2 145 | ``` 146 | 147 | Install globally: 148 | 149 | ```sh 150 | nimby install -g cligen 151 | ``` 152 | 153 | Nimby installs packages in parallel and updates your `nim.cfg` automatically. 154 | If it finds a `.nimble` file with version rules that do not match, it will warn you but still install HEAD, since that is what actually works in practice. 155 | 156 | --- 157 | 158 | ## Working with lock files 159 | 160 | Lock files make CI and reproducible builds easy. 161 | During development, you let packages float and track HEAD. 162 | When you need a reproducible build, you freeze the exact commits. 163 | 164 | Generate a lock file: 165 | 166 | ```sh 167 | nimby lock 168 | ``` 169 | 170 | Install from a lock file: 171 | 172 | ```sh 173 | nimby sync 174 | ``` 175 | 176 | This is similar to how Cargo, npm, and other package managers use lock files, but kept as simple text that lists package names, URLs, and commits. 177 | 178 | --- 179 | 180 | ## Other commands 181 | 182 | List all installed packages: 183 | 184 | ```sh 185 | nimby list 186 | ``` 187 | 188 | View dependency tree: 189 | 190 | ```sh 191 | nimby tree fidget2 192 | ``` 193 | 194 | Check workspace health: 195 | 196 | ```sh 197 | nimby doctor 198 | ``` 199 | 200 | `nimby doctor` will report missing folders, broken git repos, and out-of-sync paths in your `nim.cfg`. 201 | -------------------------------------------------------------------------------- /src/nimby.nim: -------------------------------------------------------------------------------- 1 | 2 | 3 | # To make Nimby easy to install, it depends only on system packages. 4 | import std/[os, json, times, osproc, parseopt, strutils, strformat, streams, 5 | locks] 6 | 7 | when defined(monkey): 8 | # Monkey mode: Randomly raise errors to test error handling and robustness. 9 | import std/random 10 | const MonkeyProbability = 10 11 | randomize() 12 | 13 | const 14 | WorkerCount = 32 15 | 16 | type 17 | NimbyError* = object of CatchableError 18 | 19 | Dependency* = object 20 | name*: string 21 | op*: string 22 | version*: string 23 | 24 | NimbleFile* = ref object 25 | version*: string 26 | srcDir*: string 27 | installDir*: string 28 | nimDependency*: Dependency 29 | dependencies*: seq[Dependency] 30 | 31 | var 32 | verbose: bool = false 33 | global: bool = false 34 | source: bool = false 35 | updatedGlobalPackages: bool = false 36 | timeStarted: float64 37 | 38 | printLock: Lock 39 | jobLock: Lock 40 | retryLock: Lock 41 | 42 | jobQueue: array[100, string] 43 | jobQueueStart: int = 0 44 | jobQueueEnd: int = 0 45 | jobsInProgress: int 46 | 47 | initLock(jobLock) 48 | initLock(printLock) 49 | initLock(retryLock) 50 | 51 | template withLock(lock: Lock, body: untyped) = 52 | ## Acquire the lock and execute the body. 53 | acquire(lock) 54 | {.gcsafe.}: 55 | try: 56 | body 57 | finally: 58 | release(lock) 59 | 60 | proc info(message: string) = 61 | ## Print an informational message if verbose is true. 62 | if verbose: 63 | # To prevent garbled output, lock the print lock. 64 | withLock(printLock): 65 | echo message 66 | 67 | proc print(message: string) = 68 | ## Print a log message. 69 | # To prevent garbled output, lock the print lock. 70 | withLock(printLock): 71 | echo message 72 | 73 | proc readFileSafe(fileName: string): string = 74 | ## Read the file and return the content. 75 | try: 76 | when defined(monkey): 77 | if rand(100) < MonkeyProbability: 78 | raise newException(NimbyError, "error reading file `" & fileName & "`: " & "Monkey error") 79 | return readFile(fileName) 80 | except: 81 | # Lock is normally not needed, but if we are retrying, lets be double safe. 82 | withLock(retryLock): 83 | for trying in 2 .. 3: 84 | print &"Try {trying} of 3: {getCurrentExceptionMsg()}" 85 | try: 86 | return readFile(fileName) 87 | except: 88 | sleep(100 * trying) 89 | raise newException(NimbyError, "error reading file `" & fileName & "`: " & getCurrentExceptionMsg()) 90 | 91 | proc writeFileSafe(fileName: string, content: string) = 92 | ## Write the file and return the content. 93 | try: 94 | when defined(monkey): 95 | if rand(100) < MonkeyProbability: 96 | raise newException(NimbyError, "error writing file `" & fileName & "`: " & "Monkey error") 97 | writeFile(fileName, content) 98 | except: 99 | # Lock is normally not needed, but if we are retrying, lets be double safe. 100 | withLock(retryLock): 101 | for trying in 2 .. 3: 102 | print &"Try {trying} of 3: {getCurrentExceptionMsg()}" 103 | try: 104 | writeFile(fileName, content) 105 | return 106 | except: 107 | sleep(100 * trying) 108 | raise newException(NimbyError, "error writing file `" & fileName & "`: " & getCurrentExceptionMsg()) 109 | 110 | proc runOnce(command: string) = 111 | let exeName = command.split(" ")[0] 112 | let args = command.split(" ")[1..^1] 113 | try: 114 | var options = {poUsePath} 115 | if verbose: 116 | # Print the command output to the console. 117 | options.incl(poStdErrToStdOut) 118 | options.incl(poParentStreams) 119 | if verbose: 120 | print "> " & command 121 | let p = startProcess(exeName, args=args, options=options) 122 | if p.waitForExit(-1) != 0: 123 | if not verbose: 124 | print "> " & command 125 | print p.peekableOutputStream().readAll() 126 | print p.peekableErrorStream().readAll() 127 | raise newException(NimbyError, "error code: " & $p.peekExitCode()) 128 | p.close() 129 | except: 130 | raise newException(NimbyError, "error running command `" & command & "`: " & $getCurrentExceptionMsg()) 131 | 132 | proc runSafe(command: string) = 133 | ## Run the command and print the output if it fails. 134 | try: 135 | when defined(monkey): 136 | if rand(100) < MonkeyProbability: 137 | raise newException(NimbyError, "error starting process `" & command & "`: " & "Monkey error") 138 | runOnce(command) 139 | except: 140 | # Lock is normally not needed, but if we are retrying, lets be double safe. 141 | withLock(retryLock): 142 | for trying in 2 .. 3: 143 | print &"Try {trying} of 3: {getCurrentExceptionMsg()}" 144 | try: 145 | runOnce(command) 146 | return 147 | except: 148 | sleep(100 * trying) 149 | raise newException(NimbyError, "error running command `" & command & "`: " & getCurrentExceptionMsg()) 150 | 151 | proc timeStart() = 152 | ## Start the timer. 153 | timeStarted = epochTime() 154 | 155 | proc timeEnd() = 156 | ## Stop the timer and print the time taken. 157 | let timeEnded = epochTime() 158 | let dt = timeEnded - timeStarted 159 | print &"Took: {dt:.2f} seconds" 160 | 161 | proc writeVersion() = 162 | ## Print the version of Nimby. 163 | print "Nimby 0.1.13" 164 | 165 | proc writeHelp() = 166 | ## Show the help message. 167 | print "Usage: nimby [options]" 168 | print " ~ Minimal package manager for Nim. ~" 169 | print " -g, --global Install packages in the ~/.nimby/pkgs directory" 170 | print " -v, --version print the version of Nimby" 171 | print " -h, --help show this help message" 172 | print " -V, --verbose print verbose output" 173 | print "Subcommands:" 174 | print " install install all Nim packages in the current directory" 175 | print " update update all Nim packages in the current directory" 176 | print " remove remove all Nim packages in the current directory" 177 | print " list list all Nim packages in the current directory" 178 | print " tree show all packages as a dependency tree" 179 | print " doctor diagnose all packages and fix linking issues" 180 | print " lock generate a lock file for a package" 181 | print " sync synchronize packages from a lock file" 182 | print " help show this help message" 183 | 184 | proc getGlobalPackagesDir(): string = 185 | ## Get the global packages directory. 186 | "~/.nimby/pkgs".expandTilde() 187 | 188 | proc parseNimbleFile*(fileName: string): NimbleFile = 189 | ## Parse the .nimble file and return a NimbleFile object. 190 | let nimble = readFileSafe(fileName) 191 | result = NimbleFile(installDir: fileName.parentDir()) 192 | for line in nimble.splitLines(): 193 | if line.startsWith("version"): 194 | result.version = line.split(" ")[^1].strip().replace("\"", "") 195 | elif line.startsWith("srcDir"): 196 | result.srcDir = line.split(" ")[^1].strip().replace("\"", "") 197 | elif line.startsWith("requires"): 198 | var i = 9 199 | var name, op, version = "" 200 | while i < line.len and line[i] in [' ', '"']: 201 | inc i 202 | while i < line.len and line[i] notin ['=', '<', '>', '~', '^', ' ', '"']: 203 | name.add(line[i]) 204 | inc i 205 | while i < line.len and line[i] in [' ']: 206 | inc i 207 | while i < line.len and line[i] in ['=', '<', '>', '~', '^']: 208 | op.add(line[i]) 209 | inc i 210 | while i < line.len and line[i] in [' ']: 211 | inc i 212 | while i < line.len and line[i] notin ['"']: 213 | version.add(line[i]) 214 | inc i 215 | let dep = Dependency( 216 | name: name, 217 | op: op, 218 | version: version 219 | ) 220 | if name == "nim": 221 | result.nimDependency = dep 222 | else: 223 | result.dependencies.add(dep) 224 | return result 225 | 226 | proc getNimbleFile(name: string): NimbleFile = 227 | ## Get the .nimble file for a package. 228 | let 229 | localPath = name / name & ".nimble" 230 | globalPath = getGlobalPackagesDir() / name / name & ".nimble" 231 | if fileExists(localPath): 232 | return parseNimbleFile(localPath) 233 | if fileExists(globalPath): 234 | return parseNimbleFile(globalPath) 235 | 236 | proc getGlobalPackages(): JsonNode = 237 | ## Fetch and return the global packages index (packages.json). 238 | let globalPackagesDir = getGlobalPackagesDir() / "packages" 239 | if not updatedGlobalPackages: 240 | if not fileExists(globalPackagesDir / "packages.json"): 241 | info "Packages.json not found, cloning..." 242 | withLock(jobLock): 243 | if not fileExists(globalPackagesDir / "packages.json") and not updatedGlobalPackages: 244 | runOnce(&"git clone https://github.com/nim-lang/packages.git --depth 1 {globalPackagesDir}") 245 | updatedGlobalPackages = true 246 | else: 247 | info "Packages.json found, pulling..." 248 | withLock(jobLock): 249 | if not updatedGlobalPackages: 250 | runSafe(&"git -C {globalPackagesDir} pull") 251 | updatedGlobalPackages = true 252 | 253 | return readFileSafe(globalPackagesDir & "/packages.json").parseJson() 254 | 255 | proc getGlobalPackage(packageName: string): JsonNode = 256 | ## Get a global package from the global packages.json file. 257 | let packages = getGlobalPackages() 258 | for p in packages: 259 | if p["name"].getStr() == packageName: 260 | return p 261 | 262 | proc fetchPackage(argument: string) {.gcsafe.} 263 | proc addTreeToConfig(path: string) {.gcsafe.} 264 | 265 | proc enqueuePackage(packageName: string) = 266 | ## Add a package to the job queue. 267 | withLock(jobLock): 268 | if packageName notin jobQueue: 269 | jobQueue[jobQueueEnd] = packageName 270 | inc jobQueueEnd 271 | else: 272 | info &"Package already in queue: {packageName}" 273 | 274 | proc popPackage(): string = 275 | ## Pop a package from the job queue or return an empty string. 276 | withLock(jobLock): 277 | if jobQueueEnd > jobQueueStart: 278 | result = jobQueue[jobQueueStart] 279 | inc jobQueueStart 280 | inc jobsInProgress 281 | 282 | proc readGitHash(packageName: string): string = 283 | ## Read the Git hash of a package. 284 | let globalPath = getGlobalPackagesDir() / packageName 285 | for path in [packageName, globalPath]: 286 | if dirExists(path): 287 | let p = execCmdEx(&"git -C {path} rev-parse HEAD") 288 | if p.exitCode == 0: 289 | return p.output.strip() 290 | return "" 291 | 292 | proc readPackageUrl(packageName: string): string = 293 | ## Read the URL of a package. 294 | let packages = getGlobalPackages() 295 | for p in packages: 296 | if p["name"].getStr() == packageName: 297 | return p["url"].getStr() 298 | 299 | proc fetchDeps(packageName: string) = 300 | ## Fetch the dependencies of a package. 301 | let package = getNimbleFile(packageName) 302 | if package == nil: 303 | quit(&"Can't fetch deps for: Nimble file not found: {packageName}") 304 | for dep in package.dependencies: 305 | info &"Dependency: {dep}" 306 | enqueuePackage(dep.name) 307 | 308 | proc worker(id: int) {.thread.} = 309 | ## Worker thread that processes packages from the queue. 310 | while true: 311 | let pkg = popPackage() 312 | if pkg.len == 0: 313 | var done: bool 314 | withLock(jobLock): 315 | done = (jobsInProgress == 0) 316 | if done: 317 | break 318 | sleep(20) 319 | continue 320 | 321 | if dirExists(pkg): 322 | withLock(jobLock): 323 | dec jobsInProgress 324 | info &"Package already exists: {pkg}" 325 | addTreeToConfig(pkg) 326 | continue 327 | 328 | fetchPackage(pkg) 329 | 330 | withLock(jobLock): 331 | dec jobsInProgress 332 | 333 | proc addConfigDir(path: string) = 334 | ## Add a directory to the nim.cfg file. 335 | withLock(jobLock): 336 | let path = path.replace("\\", "/") # Always use Linux-style paths. 337 | if not fileExists("nim.cfg"): 338 | writeFileSafe("nim.cfg", "# Created by Nimby\n") 339 | var nimCfg = readFileSafe("nim.cfg") 340 | if nimCfg.contains(&"--path:\"{path}\""): 341 | return 342 | let line = &"--path:\"{path}\"\n" 343 | info &"Adding nim.cfg line: {line.strip()}" 344 | nimCfg.add(line) 345 | writeFileSafe("nim.cfg", nimCfg) 346 | 347 | proc addConfigPackage(name: string) = 348 | ## Add a package to the nim.cfg file. 349 | let package = getNimbleFile(name) 350 | if package == nil: 351 | quit(&"Can't add config package: Nimble file not found: {name}") 352 | addConfigDir(package.installDir / package.srcDir) 353 | 354 | proc removeConfigDir(path: string) = 355 | ## Remove a directory from the nim.cfg file. 356 | withLock(jobLock): 357 | var nimCfg = readFileSafe("nim.cfg") 358 | var lines = nimCfg.splitLines() 359 | for i, line in lines: 360 | if line.contains(&"--path:\"{path}\""): 361 | lines.delete(i) 362 | break 363 | nimCfg = lines.join("\n") 364 | writeFileSafe("nim.cfg", lines.join("\n")) 365 | 366 | proc removeConfigPackage(name: string) = 367 | ## Remove the package from the nim.cfg file. 368 | let package = getNimbleFile(name) 369 | if package == nil: 370 | quit(&"Can't remove config package: Nimble file not found: {name}") 371 | removeConfigDir(package.installDir / package.srcDir) 372 | 373 | proc addTreeToConfig(path: string) = 374 | ## Add the tree of a package to the nim.cfg file. 375 | let nimbleFile = getNimbleFile(path) 376 | if nimbleFile == nil: 377 | quit(&"Can't add tree to config: Nimble file not found: {path}") 378 | addConfigDir(nimbleFile.installDir / nimbleFile.srcDir) 379 | for dependency in nimbleFile.dependencies: 380 | enqueuePackage(dependency.name) 381 | 382 | proc fetchPackage(argument: string) = 383 | ## Main recursive function to fetch a package and its dependencies. 384 | if argument.endsWith(".nimble"): 385 | # Package from a Nimble file. 386 | let nimblePath = argument 387 | if not fileExists(nimblePath): 388 | quit(&"Local .nimble file not found: {nimblePath}") 389 | else: 390 | info &"Using local .nimble file: {nimblePath}" 391 | let 392 | packageName = nimblePath.splitFile().name 393 | packagePath = nimblePath.parentDir() 394 | addConfigDir(packagePath) 395 | for dependency in parseNimbleFile(nimblePath).dependencies: 396 | enqueuePackage(dependency.name) 397 | 398 | elif argument.contains(" "): 399 | 400 | # Install a locked package. 401 | let 402 | parts = argument.split(" ") 403 | packageName = parts[0] 404 | packageUrl = parts[2] 405 | packageGitHash = parts[3] 406 | packagePath = 407 | if global: 408 | getGlobalPackagesDir() / packageName 409 | else: 410 | packageName 411 | 412 | info &"Looking in directory: {packagePath}" 413 | 414 | if not dirExists(packagePath): 415 | # Clone the package from the URL at the given Git hash. 416 | runOnce(&"git clone --no-checkout --depth 1 {packageUrl} {packagePath}") 417 | runOnce(&"git -C {packagePath} fetch --depth 1 origin {packageGitHash}") 418 | runOnce(&"git -C {packagePath} checkout {packageGitHash}") 419 | print &"Installed package: {packageName}" 420 | else: 421 | # Check whether the package is at the given Git hash. 422 | let gitHash = readGitHash(packageName) 423 | if gitHash != packageGitHash: 424 | runSafe(&"git -C {packagePath} fetch --depth 1 origin {packageGitHash}") 425 | runSafe(&"git -C {packagePath} checkout {packageGitHash}") 426 | print &"Updated package: {packageName}" 427 | else: 428 | info &"Package {packageName} has the correct hash." 429 | addConfigPackage(packageName) 430 | 431 | else: 432 | 433 | # Install a global or local package. 434 | let package = getGlobalPackage(argument) 435 | if package == nil: 436 | quit &"Package `{argument}` not found in global packages." 437 | let 438 | name = package["name"].getStr() 439 | methodKind = package["method"].getStr() 440 | url = package["url"].getStr() 441 | info &"Package: {name} {methodKind} {url}" 442 | case methodKind: 443 | of "git": 444 | let path = 445 | if global: 446 | getGlobalPackagesDir() / name 447 | else: 448 | name 449 | info &"Cloning package: {argument} to {path}" 450 | if dirExists(path): 451 | info &"Package already exists: {path}" 452 | else: 453 | runOnce(&"git clone --depth 1 {url} {path}") 454 | addConfigPackage(name) 455 | print &"Installed package: {name}" 456 | fetchDeps(name) 457 | else: 458 | quit &"Unknown method {methodKind} for fetching package {name}" 459 | 460 | proc installPackage(argument: string) = 461 | ## Install a package. 462 | timeStart() 463 | print &"Installing package: {argument}" 464 | 465 | if dirExists(argument): 466 | quit("Package already installed.") 467 | 468 | # init job queue 469 | jobQueueStart = 0 470 | jobQueueEnd = 0 471 | jobsInProgress = 0 472 | 473 | # Ensure the packages index is available before workers start. 474 | # Enqueue the initial package. 475 | enqueuePackage(argument) 476 | 477 | var threads: array[WorkerCount, Thread[int]] 478 | for i in 0 ..< WorkerCount: 479 | createThread(threads[i], worker, i) 480 | for i in 0 ..< WorkerCount: 481 | joinThread(threads[i]) 482 | 483 | timeEnd() 484 | quit(0) 485 | 486 | proc updatePackage(argument: string) = 487 | ## Update a package. 488 | if argument == "": 489 | quit("No package specified for update") 490 | info &"Updating package: {argument}" 491 | let package = getNimbleFile(argument) 492 | if package == nil: 493 | quit(&"Can't update package: Nimble file not found: {argument}") 494 | let packagePath = package.installDir 495 | if not dirExists(packagePath): 496 | quit(&"Package not found: {packagePath}") 497 | runSafe(&"git -C {packagePath} pull") 498 | print &"Updated package: {argument}" 499 | 500 | proc removePackage(argument: string) = 501 | ## Remove a package. 502 | if argument == "": 503 | quit("No package specified for removal") 504 | info &"Removing package: {argument}" 505 | removeConfigPackage(argument) 506 | let package = getNimbleFile(argument) 507 | if package == nil: 508 | quit(&"Can't remove package: Nimble file not found: {argument}") 509 | let packagePath = package.installDir 510 | if not dirExists(packagePath): 511 | quit(&"Package not found: {packagePath}") 512 | removeDir(packagePath) 513 | print &"Removed package: {argument}" 514 | 515 | proc listPackage(argument: string) = 516 | ## List a package. 517 | let nimbleFile = getNimbleFile(argument) 518 | if nimbleFile != nil: 519 | let packageName = argument 520 | let packageVersion = nimbleFile.version 521 | let gitUrl = readPackageUrl(packageName) 522 | let gitHash = readGitHash(packageName) 523 | print &"{packageName} {packageVersion} {gitUrl} {gitHash}" 524 | 525 | proc listPackages(argument: string) = 526 | ## List all packages in the workspace. 527 | if argument != "": 528 | listPackage(argument) 529 | else: 530 | for dir in [".", getGlobalPackagesDir()]: 531 | for kind, path in walkDir(dir): 532 | if kind == pcDir: 533 | listPackage(path.extractFilename()) 534 | 535 | proc treePackage(name, indent: string) = 536 | ## Walk the tree of a package. 537 | let nimbleFile = getNimbleFile(name) 538 | if nimbleFile != nil: 539 | let packageName = name 540 | let packageVersion = nimbleFile.version 541 | print &"{indent}{packageName} {packageVersion}" 542 | for dependency in nimbleFile.dependencies: 543 | treePackage(dependency.name, indent & " ") 544 | 545 | proc treePackages(argument: string) = 546 | ## Tree the package dependencies. 547 | if argument != "": 548 | treePackage(argument, "") 549 | else: 550 | for dir in [".", getGlobalPackagesDir()]: 551 | for kind, path in walkDir(dir): 552 | if kind == pcDir: 553 | treePackage(path.extractFilename(), "") 554 | 555 | proc checkPackage(packageName: string) = 556 | ## Check a package. 557 | let nimbleFile = getNimbleFile(packageName) 558 | if nimbleFile == nil: 559 | print &"Package `{packageName}` is not a Nim project (no .nimble file found)." 560 | return 561 | for dependency in nimbleFile.dependencies: 562 | if not dirExists(dependency.name): 563 | print &"Dependency `{dependency.name}` not found for package `{packageName}`." 564 | if not fileExists(&"nim.cfg"): 565 | quit(&"Package `nim.cfg` not found.") 566 | let nimCfg = readFileSafe("nim.cfg") 567 | if not nimCfg.contains(&"--path:\"{packageName}/") and not nimCfg.contains(&"--path:\"{packageName}\""): 568 | print &"Package `{packageName}` not found in nim.cfg." 569 | 570 | proc doctorPackage(argument: string) = 571 | ## Diagnose packages and fix configuration issues. 572 | # Walk through all packages. 573 | # Ensure the workspace root has a nim.cfg entry. 574 | # Ensure all dependencies are installed. 575 | if argument != "": 576 | if not dirExists(argument): 577 | quit(&"Package `{argument}` not found.") 578 | let packageName = argument 579 | checkPackage(packageName) 580 | else: 581 | for kind, path in walkDir("."): 582 | if kind == pcDir: 583 | let packageName = path.extractFilename() 584 | checkPackage(packageName) 585 | 586 | proc lockPackage(argument: string) = 587 | ## Generate a lock file for a package. 588 | for packageName in [argument, getGlobalPackagesDir() / argument]: 589 | let nimbleFile = getNimbleFile(packageName) 590 | if nimbleFile == nil: 591 | continue 592 | var listedDeps: seq[string] 593 | proc walkDeps(packageName: string) = 594 | for dependency in getNimbleFile(packageName).dependencies: 595 | if dependency.name notin listedDeps: 596 | let url = readPackageUrl(dependency.name) 597 | let version = getNimbleFile(dependency.name).version 598 | let gitHash = readGitHash(dependency.name) 599 | print &"{dependency.name} {version} {url} {gitHash}" 600 | listedDeps.add(dependency.name) 601 | walkDeps(dependency.name) 602 | walkDeps(packageName) 603 | break 604 | 605 | proc syncPackage(path: string) = 606 | ## Synchronize packages from a lock file. 607 | info &"Syncing lock file: {path}" 608 | timeStart() 609 | 610 | if not fileExists(path): 611 | quit(&"Package lock file `{path}` not found.") 612 | 613 | for line in readFileSafe(path).splitLines(): 614 | let parts = line.split(" ") 615 | if parts.len != 4: 616 | continue 617 | info "Syncing package: " & line 618 | enqueuePackage(line) 619 | 620 | var threads: array[WorkerCount, Thread[int]] 621 | for i in 0 ..< WorkerCount: 622 | createThread(threads[i], worker, i) 623 | for i in 0 ..< WorkerCount: 624 | joinThread(threads[i]) 625 | 626 | timeEnd() 627 | quit(0) 628 | 629 | proc installNim(nimVersion: string) = 630 | ## Install a specific version of Nim. 631 | info &"Installing Nim: {nimVersion}" 632 | let nimbyDir = "~/.nimby".expandTilde() 633 | if not dirExists(nimbyDir): 634 | createDir(nimbyDir) 635 | let installDir = nimbyDir / ("nim-" & nimVersion) 636 | 637 | if dirExists(installDir): 638 | info &"Nim {nimVersion} already downloaded at: {installDir}" 639 | else: 640 | createDir(installDir) 641 | 642 | let previousDir = getCurrentDir() 643 | setCurrentDir(installDir) 644 | 645 | if source: 646 | runOnce(&"git clone https://github.com/nim-lang/Nim.git --branch v{nimVersion} --depth 1 {installDir}") 647 | setCurrentDir(installDir) 648 | when defined(windows): 649 | runSafe("build_all.bat") 650 | else: 651 | runSafe("./build_all.sh") 652 | let keepDirsAndFiles = @[ 653 | "bin", 654 | "compiler", 655 | "config", 656 | "lib", 657 | "copying.txt" 658 | ] 659 | for kind, path in walkDir(installDir): 660 | if path.extractFilename() notin keepDirsAndFiles: 661 | info &"Cleaning up: {path}" 662 | if kind == pcDir: 663 | removeDir(path) 664 | else: 665 | removeFile(path) 666 | else: 667 | when defined(windows): 668 | let url = &"https://nim-lang.org/download/nim-{nimVersion}_x64.zip" 669 | print &"Downloading: {url}" 670 | runSafe(&"curl -sSL {url} -o nim.zip") 671 | runSafe("powershell -NoProfile -Command Expand-Archive -Force -Path nim.zip -DestinationPath .") 672 | let extractedDir = &"nim-{nimVersion}" 673 | if dirExists(extractedDir): 674 | for kind, path in walkDir(extractedDir): 675 | let name = path.extractFilename() 676 | if kind == pcDir: 677 | moveDir(extractedDir / name, installDir / name) 678 | else: 679 | moveFile(extractedDir / name, installDir / name) 680 | removeDir(extractedDir) 681 | 682 | elif defined(macosx): 683 | let url = &"https://github.com/treeform/nimbuilds/raw/refs/heads/master/nim-{nimVersion}-macosx_arm64.tar.xz" 684 | print &"Downloading: {url}" 685 | runSafe(&"curl -sSL {url} -o nim.tar.xz") 686 | print "Extracting the Nim compiler" 687 | runSafe("tar xf nim.tar.xz --strip-components=1") 688 | 689 | elif defined(linux): 690 | let url = &"https://nim-lang.org/download/nim-{nimVersion}-linux_x64.tar.xz" 691 | print &"Downloading: {url}" 692 | runSafe(&"curl -sSL {url} -o nim.tar.xz") 693 | print "Extracting the Nim compiler" 694 | runSafe("tar xf nim.tar.xz --strip-components=1") 695 | 696 | else: 697 | quit "Unsupported platform for Nim installation" 698 | 699 | setCurrentDir(previousDir) 700 | print &"Installed Nim {nimVersion} to: {installDir}" 701 | 702 | # copy nim-{nimVersion} to global nim directory 703 | let versionNimDir = nimbyDir / "nim-" & nimVersion 704 | let globalNimDir = nimbyDir / "nim" 705 | removeDir(globalNimDir) 706 | copyDir(versionNimDir, globalNimDir) 707 | print &"Copied {versionNimDir} to {globalNimDir}" 708 | 709 | when not defined(windows): 710 | # Make sure the Nim binary is executable. 711 | runSafe(&"chmod +x {globalNimDir}/bin/nim") 712 | 713 | # Tell the user a single PATH change they can run now. 714 | let pathEnv = getEnv("PATH") 715 | let binPath = nimbyDir / "nim" / "bin" 716 | info &"Checking if Nim is in the PATH: {pathEnv}" 717 | if not pathEnv.contains(binPath): 718 | print "Add Nim to your PATH for this session with one of:" 719 | when defined(windows): 720 | let winBin = (binPath.replace("/", "\\")) 721 | print &"$env:PATH = \"{winBin};$env:PATH\" # PowerShell" 722 | else: 723 | print &"export PATH=\"{binPath}:$PATH\" # bash/zsh" 724 | print &"fish_add_path {binPath} # fish" 725 | 726 | when isMainModule: 727 | 728 | var subcommand, argument: string 729 | var p = initOptParser() 730 | for kind, key, val in p.getopt(): 731 | case kind 732 | of cmdArgument: 733 | if subcommand == "": 734 | subcommand = key 735 | else: 736 | argument = key 737 | of cmdLongOption, cmdShortOption: 738 | case key 739 | of "help", "h": 740 | writeHelp() 741 | quit(0) 742 | of "version", "v": 743 | writeVersion() 744 | quit(0) 745 | of "verbose", "V": 746 | verbose = true 747 | of "global", "g": 748 | print "Using global packages directory." 749 | global = true 750 | if not dirExists(getGlobalPackagesDir()): 751 | info &"Creating global packages directory: {getGlobalPackagesDir()}" 752 | createDir(getGlobalPackagesDir()) 753 | of "source", "s": 754 | source = true 755 | else: 756 | print "Unknown option: " & key 757 | quit(1) 758 | of cmdEnd: 759 | assert(false) # cannot happen 760 | 761 | case subcommand 762 | of "": writeHelp() 763 | of "install": installPackage(argument) 764 | of "sync": syncPackage(argument) 765 | of "update": updatePackage(argument) 766 | of "remove", "uninstall": removePackage(argument) 767 | of "list": listPackages(argument) 768 | of "tree": treePackages(argument) 769 | of "lock": lockPackage(argument) 770 | of "use": installNim(argument) 771 | of "doctor": doctorPackage(argument) 772 | of "help": writeHelp() 773 | else: 774 | quit "Invalid command" 775 | --------------------------------------------------------------------------------