├── .github ├── dependabot.yml └── workflows │ ├── binaries.yml │ └── ci.yml ├── .gitignore ├── atlas.nimble ├── config.nims ├── doc └── atlas.md ├── license.txt ├── plugins ├── cmake.nims └── scons.nims ├── readme.md ├── src ├── atlas.nim ├── atlas.nim.cfg ├── basic │ ├── compiledpatterns.nim │ ├── compilerversions.nim │ ├── configutils.nim │ ├── context.nim │ ├── depgraphtypes.nim │ ├── deptypes.nim │ ├── deptypesjson.nim │ ├── gitops.nim │ ├── lockfiletypes.nim │ ├── nimblechecksums.nim │ ├── nimblecontext.nim │ ├── nimbleparser.nim │ ├── osutils.nim │ ├── packageinfos.nim │ ├── parse_requires.nim │ ├── pkgurls.nim │ ├── reporters.nim │ ├── sattypes.nim │ └── versions.nim ├── confighandler.nim ├── dependencies.nim ├── depgraphs.nim ├── lockfiles.nim ├── nimenv.nim ├── pkgsearch.nim └── runners.nim └── tests ├── config.nims ├── githttpserver.nim ├── tbasics.nim ├── tbasics.nims ├── testFeatures.nim ├── test_data ├── bad.nimble ├── jester.nimble ├── jester_boolean.nimble ├── jester_combined.nimble ├── jester_feature.nimble ├── jester_inverted.nimble └── query_github_balls.json ├── tester.nim ├── testerutils.nim ├── testintegration.nim ├── testlinkintegration.nim ├── testsemver.nim ├── testsemverUnit.nim ├── testtraverse.nim ├── tgitops.nim ├── tnimbleparser.nim ├── tserde.nim ├── ttnimbleparser.nims ├── ws_basic ├── atlas.config ├── deps │ └── foobar.nimble-link ├── packages │ └── packages.json └── remote-deps │ └── foobar │ └── foobar.nimble ├── ws_conflict ├── atlas.config ├── expected │ ├── deps.dot │ ├── myproject.nimble │ └── nim.cfg ├── source │ ├── apkg │ │ └── apkg.nimble │ ├── bpkg@1.0 │ │ └── bpkg.nimble │ ├── cpkg@1.0 │ │ └── cpkg.nimble │ ├── cpkg@2.0 │ │ └── cpkg.nimble │ └── dpkg │ │ └── dpkg.nimble └── url.rules ├── ws_features ├── .gitkeep ├── atlas.config └── ws_features.nimble ├── ws_features_global ├── .gitkeep ├── atlas.config └── ws_features_global.nimble ├── ws_integration ├── .gitignore ├── atlas.config ├── expected │ └── nim.cfg ├── nimble.paths └── ws_integration.nimble ├── ws_link_integration ├── deps │ └── atlas.config └── ws_link_integration.nimble ├── ws_link_semver ├── .gitkeep ├── atlas.config └── ws_link_semver.nimble ├── ws_minproject1 ├── .gitignore └── atlas.config ├── ws_semproject1 ├── .gitignore └── atlas.config ├── ws_semproject2 ├── .gitignore └── atlas.config ├── ws_semver ├── atlas.config ├── expected │ ├── deps.dot │ ├── myproject.nimble │ └── nim.cfg └── url.rules ├── ws_semver2 ├── atlas.config └── url.rules ├── ws_semver_unit ├── .gitkeep ├── atlas.config └── ws_semver_unit.nimble ├── ws_testtraverse ├── .gitkeep ├── atlas.config └── ws_testtraverse.nimble ├── ws_testtraverse_explicit ├── .gitignore ├── .gitkeep └── atlas.config └── ws_testtraverselinked └── ws_testtraverselinked.nimble /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/binaries.yml: -------------------------------------------------------------------------------- 1 | name: tagging release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | target: 15 | - os: linux 16 | arch: amd64 17 | triple: x86_64-linux-gnu 18 | name: linux_x86_64 19 | - os: linux 20 | arch: aarch64 21 | triple: aarch64-linux-gnu 22 | name: linux_arm64 23 | - os: linux 24 | arch: arm 25 | triple: arm-linux-gnueabihf 26 | name: linux_arm 27 | - os: macosx 28 | arch: universal 29 | triple: x86_64-apple-darwin14 30 | name: apple_universal 31 | - os: windows 32 | arch: amd64 33 | triple: x86_64-w64-mingw32 34 | name: windows_x86_64 35 | include: 36 | - target: 37 | os: linux 38 | builder: ubuntu-latest 39 | - target: 40 | os: macosx 41 | builder: macos-latest 42 | defaults: 43 | run: 44 | shell: bash 45 | 46 | name: '${{ matrix.target.triple }}' 47 | runs-on: ${{ matrix.builder }} 48 | steps: 49 | - name: Checkout 50 | uses: actions/checkout@v4 51 | 52 | - uses: jiro4989/setup-nim-action@v2.2.0 53 | with: 54 | nim-version: "stable" 55 | 56 | - name: setup build 57 | if: ${{ matrix.builder }} == "ubuntu-latest" 58 | run: | 59 | sudo apt install gcc make gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu \ 60 | gcc-arm-linux-gnueabihf binutils-arm-linux-gnueabihf \ 61 | mingw-w64 62 | 63 | - name: build 64 | run: | 65 | OS=${{ matrix.target.os }} ARCH=${{ matrix.target.arch }} nim buildRelease 66 | 67 | - name: Compress the Nim Language Server binaries 68 | run: | 69 | tar -c -z -v -f atlas-${{ matrix.target.name }}.tar.gz `ls atlas{,.exe} 2>/dev/null || true` 70 | 71 | - name: Upload the Nim Language Server Binaries 72 | uses: actions/upload-artifact@v4 73 | with: 74 | name: atlas-${{ matrix.target.name }}.tar.gz 75 | path: atlas-${{ matrix.target.name }}.tar.gz 76 | 77 | create-github-release: 78 | name: Create Github Release 79 | needs: [build] 80 | runs-on: ubuntu-latest 81 | permissions: 82 | contents: write 83 | steps: 84 | - name: Download artefacts 85 | uses: actions/download-artifact@v4 86 | 87 | - uses: ncipollo/release-action@v1 88 | with: 89 | name: Latest Binaries 90 | artifacts: "*/*" 91 | allowUpdates: true 92 | makeLatest: true 93 | 94 | - name: Delete artefacts 95 | uses: geekyeggo/delete-artifact@v5 96 | with: 97 | failOnError: false 98 | name: "atlas-*" -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: atlas ci 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | strategy: 11 | fail-fast: false 12 | max-parallel: 20 13 | matrix: 14 | branch: [master] 15 | target: 16 | - os: linux 17 | cpu: amd64 18 | nim_branch: devel 19 | # - os: linux 20 | # cpu: i386 21 | # nim_branch: devel 22 | - os: macos 23 | cpu: amd64 24 | nim_branch: devel 25 | - os: windows 26 | cpu: amd64 27 | nim_branch: devel 28 | - os: windows 29 | cpu: i386 30 | nim_branch: devel 31 | include: 32 | - target: 33 | os: linux 34 | builder: ubuntu-latest 35 | - target: 36 | os: macos 37 | builder: macos-latest 38 | - target: 39 | os: windows 40 | builder: windows-latest 41 | 42 | name: '${{ matrix.target.os }}-${{ matrix.target.cpu }}-nim-${{ matrix.target.nim_branch }} (${{ matrix.branch }})' 43 | runs-on: ${{ matrix.builder }} 44 | env: 45 | NIM_DIR: nim-${{ matrix.target.nim_branch }}-${{ matrix.target.cpu }} 46 | NIM_BRANCH: ${{ matrix.target.nim_branch }} 47 | NIM_ARCH: ${{ matrix.target.cpu }} 48 | steps: 49 | - name: set `core.autocrlf` to false 50 | run: | 51 | git config --global core.autocrlf false 52 | git config --global init.defaultBranch master 53 | git config --global user.email "atlasbot@nimlang.com" 54 | git config --global user.name "atlasbot" 55 | 56 | - name: Checkout atlas 57 | uses: actions/checkout@v4 58 | with: 59 | path: atlas 60 | submodules: false 61 | 62 | - name: Restore MinGW-W64 (Windows) from cache 63 | if: runner.os == 'Windows' 64 | id: windows-mingw-cache 65 | uses: actions/cache@v4 66 | with: 67 | path: external/mingw-${{ matrix.target.cpu }} 68 | key: 'mingw-${{ matrix.target.cpu }}' 69 | 70 | - name: Restore Nim DLLs dependencies (Windows) from cache 71 | if: runner.os == 'Windows' 72 | id: windows-dlls-cache 73 | uses: actions/cache@v4 74 | with: 75 | path: external/dlls-${{ matrix.target.cpu }} 76 | key: 'dlls-${{ matrix.target.cpu }}' 77 | 78 | - name: Install MinGW64 dependency (Windows) 79 | if: > 80 | steps.windows-mingw-cache.outputs.cache-hit != 'true' && 81 | runner.os == 'Windows' 82 | shell: bash 83 | run: | 84 | mkdir -p external 85 | if [[ '${{ matrix.target.cpu }}' == 'amd64' ]]; then 86 | MINGW_URL="https://github.com/brechtsanders/winlibs_mingw/releases/download/11.1.0-12.0.0-9.0.0-r2/winlibs-x86_64-posix-seh-gcc-11.1.0-mingw-w64-9.0.0-r2.7z" 87 | ARCH=64 88 | else 89 | MINGW_URL="https://github.com/brechtsanders/winlibs_mingw/releases/download/11.1.0-12.0.0-9.0.0-r2/winlibs-i686-posix-dwarf-gcc-11.1.0-mingw-w64-9.0.0-r2.7z" 90 | ARCH=32 91 | fi 92 | curl -L "$MINGW_URL" -o "external/mingw-${{ matrix.target.cpu }}.7z" 93 | 7z x -y "external/mingw-${{ matrix.target.cpu }}.7z" -oexternal/ 94 | mv external/mingw$ARCH external/mingw-${{ matrix.target.cpu }} 95 | 96 | - name: Install DLLs dependencies (Windows) 97 | if: > 98 | steps.windows-dlls-cache.outputs.cache-hit != 'true' && 99 | runner.os == 'Windows' 100 | shell: bash 101 | run: | 102 | mkdir -p external 103 | curl -L "https://nim-lang.org/download/windeps.zip" -o external/windeps.zip 104 | 7z x -y external/windeps.zip -oexternal/dlls-${{ matrix.target.cpu }} 105 | 106 | - name: Path to cached dependencies (Windows) 107 | if: > 108 | runner.os == 'Windows' 109 | shell: bash 110 | run: | 111 | echo '${{ github.workspace }}'"/external/mingw-${{ matrix.target.cpu }}/bin" >> $GITHUB_PATH 112 | echo '${{ github.workspace }}'"/external/dlls-${{ matrix.target.cpu }}" >> $GITHUB_PATH 113 | 114 | # - name: Restore Nim from cache 115 | # if: > 116 | # steps.nim-compiler-cache.outputs.cache-hit != 'true' && 117 | # matrix.target.nim_branch != 'devel' 118 | # id: nim-compiler-cache 119 | # uses: actions/cache@v2 120 | # with: 121 | # path: '${{ github.workspace }}/nim-${{ matrix.target.nim_branch }}-${{ matrix.target.cpu }}' 122 | # key: 'nim-${{ matrix.target.cpu }}-${{ matrix.target.nim_branch }}' 123 | 124 | - name: Setup Nim 125 | uses: alaviss/setup-nim@0.1.1 126 | with: 127 | path: 'nim' 128 | version: ${{ matrix.target.nim_branch }} 129 | architecture: ${{ matrix.target.cpu }} 130 | 131 | - name: Setup Test Repos 132 | run: | 133 | cd atlas 134 | nim testReposSetup 135 | 136 | - name: Install SAT 137 | run: | 138 | # cd atlas 139 | # nimble install -y sat 140 | git clone https://github.com/nim-lang/sat.git 141 | 142 | - name: Run tests 143 | run: | 144 | cd atlas 145 | nim test 146 | 147 | - name: Run Docs 148 | run: | 149 | cd atlas 150 | nim docs 151 | 152 | - name: Test install with Nimble 153 | run: | 154 | cd atlas 155 | nimble install -y 156 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache/ 2 | nimblecache/ 3 | workdir/ 4 | htmldocs/ 5 | 6 | # Ignore files without extensions 7 | * 8 | !*/ 9 | !*.* 10 | 11 | # Ignore directories containing Git repositories 12 | */**/.git 13 | 14 | # Ignore test stuff 15 | tests/ws_*/*/ 16 | tests/ws_*/*.json 17 | 18 | # Ignore others 19 | bin/ 20 | *.exe 21 | src/atlas.exe 22 | atlas-tests/ 23 | *.zip 24 | atlas 25 | deps.png 26 | nim.cfg 27 | nimble.develop 28 | nimble.paths 29 | -------------------------------------------------------------------------------- /atlas.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | version = "0.9.0" 3 | author = "Araq" 4 | description = "Atlas is a simple package cloner tool. It manages an isolated project." 5 | license = "MIT" 6 | srcDir = "src" 7 | skipDirs = @["doc"] 8 | bin = @["atlas"] 9 | 10 | # Dependencies 11 | 12 | requires "nim >= 2.0.0" 13 | requires "sat" 14 | 15 | task docs, "build Atlas's docs": 16 | exec "nim rst2html --putenv:atlasversion=$1 doc/atlas.md" % version 17 | -------------------------------------------------------------------------------- /config.nims: -------------------------------------------------------------------------------- 1 | import std/[strformat, strutils] 2 | 3 | when defined(nimPreviewSlimSystem): 4 | import std/syncio 5 | 6 | --path:"$nim" 7 | --path:"../sat/src/" 8 | 9 | task build, "Build local atlas": 10 | exec "nim c -d:debug -o:bin/atlas src/atlas.nim" 11 | 12 | task unitTests, "Runs unit tests": 13 | exec "nim c -d:debug -r tests/tbasics.nim" 14 | exec "nim c -d:debug -r tests/tserde.nim" 15 | exec "nim c -d:debug -r tests/tgitops.nim" 16 | exec "nim c -d:debug -r tests/tnimbleparser.nim" 17 | exec "nim c -d:debug -r tests/testtraverse.nim" 18 | exec "nim c -d:debug -r tests/testsemverUnit.nim" 19 | 20 | task tester, "Runs integration tests": 21 | exec "nim c -d:debug -r tests/tester.nim" 22 | 23 | task buildRelease, "Build release": 24 | exec "nimble install -y sat" 25 | when defined(macosx): 26 | let x86Args = "\"-target x86_64-apple-macos11 -arch x86_64 -DARCH=x86_64\"" 27 | exec "nim c -d:release --passC:" & x86args & " --passL:" & x86args & " -o:./atlas_x86_64 src/atlas.nim" 28 | let armArgs = "\"-target arm64-apple-macos11 -arch arm64 -DARCH=arm64\"" 29 | exec "nim c -d:release --passC:" & armArgs & " --passL:" & armArgs & " -o:./atlas_arm64 src/atlas.nim" 30 | exec "lipo -create -output atlas atlas_x86_64 atlas_arm64" 31 | rmFile("atlas_x86_64") 32 | rmFile("atlas_arm64") 33 | else: 34 | let os = getEnv("OS") 35 | let arch = getEnv("ARCH") 36 | if os != "" and arch != "": 37 | if os == "windows": 38 | exec "nim c -d:release -d:mingw -o:./atlas src/atlas.nim" 39 | else: 40 | exec "nim c -d:release --cpu:" & arch & " --os:" & os & " -o:./atlas src/atlas.nim" 41 | else: 42 | exec "nim c -d:release -o:./atlas src/atlas.nim" 43 | 44 | task cleanTests, "Clean tests": 45 | echo "Stashing any changes to tests" 46 | exec "git stash -- tests" 47 | echo "Removing tests" 48 | rmDir("tests") 49 | echo "Checking out tests for a clean slate" 50 | exec "git checkout -- tests" 51 | 52 | task testReposSetup, "Setup atlas-tests from a cached zip": 53 | let version = "0.1.5" 54 | let repo = "https://github.com/nim-lang/atlas-tests/" 55 | let file = "atlas-tests.zip" 56 | let url = fmt"{repo}/releases/download/v{version}/{file}" 57 | if not dirExists("atlas-tests"): 58 | cleanTestsTask() 59 | echo "Downloading Test Repos zip" 60 | exec(fmt"curl -L -o {file} {url}") 61 | echo "Unzipping Test Repos" 62 | exec(fmt"unzip -o {file}") 63 | else: 64 | let actualver = 65 | if fileExists("atlas-tests/atlas_tests.nimble"): 66 | readFile("atlas-tests/atlas_tests.nimble").split("=")[^1].replace("\"","").strip() 67 | else: 68 | "0.0.0" 69 | echo "Atlas Tests: got version: ", actualver , " expected: ", version 70 | if version notin actualver: 71 | echo fmt"Atlas Tests Outdated; Updating..." 72 | echo "Downloading Test Repos zip" 73 | exec(fmt"curl -L -o {file} {url}") 74 | echo "Deleting Atlas Test Repos" 75 | exec(fmt"mv atlas-tests atlas-tests-old-{actualver}") 76 | echo "Unzipping Test Repos" 77 | exec(fmt"unzip -o {file}") 78 | 79 | task runGitHttpServer, "Run test http server": 80 | testReposSetupTask() 81 | exec "nim c -r tests/githttpserver.nim atlas-tests/ws_integration atlas-tests/ws_generated" 82 | 83 | task test, "Runs all tests": 84 | testReposSetupTask() # download atlas-tests 85 | unitTestsTask() # tester runs both 86 | testerTask() 87 | 88 | task docs, "build Atlas's docs": 89 | exec "nim rst2html --putenv:atlasversion=$1 --d:nimPreviewSlimSystem doc/atlas.md" % version 90 | 91 | # begin Nimble config (version 2) 92 | # when withDir(thisDir(), system.fileExists("nimble.paths")): 93 | # include "nimble.paths" 94 | # end Nimble config 95 | -------------------------------------------------------------------------------- /doc/atlas.md: -------------------------------------------------------------------------------- 1 | # Atlas Package Cloner 2 | 3 | Atlas is a simple package cloner tool. It manages project dependencies in an isolated `deps/` directory. 4 | 5 | Atlas is compatible with Nimble in the sense that it supports the Nimble 6 | file format. 7 | 8 | 9 | ## Concepts 10 | 11 | Atlas uses two main concepts: 12 | 13 | 1. Projects 14 | 2. Dependencies 15 | 16 | ### Projects 17 | 18 | A project is a directory that has a file `atlas.config` inside it. Use `atlas init` 19 | to create a project out of the current working directory. 20 | 21 | Projects can share dependencies and be developed locally by using the `link` command. This creates `nimble-link` files that allow projects to share their dependencies. 22 | 23 | The project structure looks like this: 24 | 25 | ``` 26 | $project / project.nimble 27 | $project / nim.cfg 28 | $project / other main project files... 29 | $project / deps / atlas.config 30 | $project / deps / dependency-A 31 | $project / deps / dependency-B 32 | $project / deps / dependency-C.nimble-link (for linked projects) 33 | ``` 34 | 35 | The deps directory can be set via `--deps:DIR` during `atlas init`. 36 | 37 | 38 | ### Dependencies 39 | 40 | Inside a project there is a `deps` directory where your dependencies are kept. It is 41 | easy to move a dependency one level up and out of the `deps` directory, turning it into a project. 42 | Likewise, you can move a project to the `deps` directory, turning it into a dependency. 43 | 44 | The only distinction between a project and a dependency is its location. For dependency resolution 45 | a project always has a higher priority than a dependency. 46 | 47 | 48 | ## No magic 49 | 50 | Atlas works by managing two files for you, the `project.nimble` file and the `nim.cfg` file. You can 51 | edit these manually too, Atlas doesn't touch what should be left untouched. 52 | 53 | 54 | ## How it works 55 | 56 | Atlas uses git commits internally; version requirements are translated 57 | to git commits via git tags and a fallback of searching Nimble file commits. 58 | 59 | Atlas uses URLs internally; Nimble package names are translated to URLs 60 | via Nimble's `packages.json` file. Atlas uses "shortnames" for known URLs from 61 | packages. Unofficial URLs, including forks, using a name triplet of the form 62 | `projectName.author.host`. For example Atlas would be `atlas.nim-lang.github.com`. Packages can be added using `nameOverrides` in `atlas.config` which adds a new name to URL mapping. 63 | 64 | Atlas does not call the Nim compiler for a build, instead it creates/patches 65 | a `nim.cfg` file for the compiler. For example: 66 | 67 | ``` 68 | ############# begin Atlas config section ########## 69 | --noNimblePath 70 | --path:"deps/nimx" 71 | --path:"deps/sdl2/src" 72 | --path:"deps/opengl/src" 73 | --path:"../linked-project/src" 74 | --path:"../linked-project/deps/msgpack4nim/" 75 | ############# end Atlas config section ########## 76 | ``` 77 | 78 | The version selection is deterministic, it picks up the *minimum* required 79 | version. Thanks to this design, lock files are much less important. 80 | 81 | 82 | 83 | ## Commands 84 | 85 | Atlas supports the following commands: 86 | 87 | 88 | ### Use / 89 | 90 | Clone the package behind `url` or `package name` and its dependencies into 91 | the `deps` directory and make it available for your current project. 92 | Atlas will create or patch the files `$project.nimble` and `nim.cfg` for you so that you can simply 93 | import the required modules. 94 | 95 | For example: 96 | 97 | ``` 98 | mkdir newproject 99 | cd newproject 100 | git init 101 | atlas use lexim 102 | # add `import lexim` to your example.nim file 103 | nim c example.nim 104 | 105 | ``` 106 | 107 | 108 | ### Link 109 | 110 | Link another project into the current project to share its dependencies. This creates `nimble-link` files that allow the projects to share their dependencies. 111 | 112 | For example: 113 | 114 | ``` 115 | atlas link ../other-project 116 | ``` 117 | 118 | This will link the other project and make its dependencies available to your current project. The other project must be another Atlas project and have a Nimble file. 119 | 120 | The linked project will be added to this project's Nimble file if it's not already present. 121 | 122 | Note, that the other project's `nameOverrides` and `urlOverrides` *aren't* imported. You may need to import the name-overrides to properly use the deps. This is due to the triplet-naming above. 123 | 124 | ### Clone/Update / 125 | 126 | Clones a URL and all of its dependencies (recursively) into the project. 127 | Creates or patches a `nim.cfg` file with the required `--path` entries. 128 | 129 | **Note**: Due to the used algorithms an `update` is the same as a `clone`. 130 | 131 | 132 | If a `` is given instead the name is first translated into an URL 133 | via `packages.json` or via a github search. 134 | 135 | ## When Statements 136 | 137 | When statements provide support for boolean expressions with a subset of compile time defines. The list of defines currently supported: 138 | 139 | windows, posix, linux, macosx, freebsd, openbsd, netbsd, solaris; 140 | amd64, x86_64, i386, arm, arm64, mips, powerpc; 141 | 142 | If a when statement isn't supported consider using `feature` statements instead. 143 | 144 | ### Feature Statements 145 | 146 | Features in Nimble files enable optional requirements for things different scenarios. This is useful when dealing with scenarios like testing only dependencies. 147 | 148 | *Note*: Currently features aren't saved to the Atlas config you must always pass `atlas --feature:foobar` when doing any command. This simplifies configuration and state management in Atlas. It only does what you ask it to do. 149 | 150 | ```nim 151 | require "normallib" 152 | 153 | feature "testing": 154 | require "mytestlib" 155 | ``` 156 | 157 | Features are lazily cloned by Atlas until they are specified by either a requires feature or passed from the command line. 158 | 159 | In Nimble files you can enable features for a a given package like so: 160 | ```nim 161 | require "somelib[testing]" 162 | require "anotherlib[testing, async]" 163 | ``` 164 | 165 | 166 | ### Search 167 | 168 | Search the package index `packages.json` for a package that contains the given terms 169 | in its description (or name or list of tags). 170 | 171 | 172 | ### Install 173 | 174 | Use the .nimble file to setup the project's dependencies. 175 | 176 | ### UpdateProjects / updateDeps [filter] 177 | 178 | Update every project / dependency in the project that has a remote URL that 179 | matches `filter` if a filter is given. The project / dependency is only updated 180 | if there are no uncommitted changes. 181 | 182 | ### Others 183 | 184 | Run `atlas --help` for more features. 185 | 186 | ## Package Overrides 187 | 188 | Sometimes two URLs can conflict for the same dependency shortname. For example, when a project uses a forked dependency with bug fixes. These conflicts need to be manually resolved using `pkgOverrides` in `atlas.config`. The format is package name and the selected URL: 189 | 190 | ```json 191 | "pkgOverrides": { 192 | "asynctools": "https://github.com/timotheecour/asynctools" 193 | }, 194 | ``` 195 | 196 | ## Overrides 197 | 198 | You can override how Atlas resolves a package name or a URL. The overrides use 199 | a simple pattern matching language and are flexible enough to integrate private 200 | gitlab repositories. 201 | 202 | ```json 203 | { 204 | "resolver": "SemVer", 205 | "nameOverrides": { 206 | "customProject": "https://gitlab.company.com/customProject" 207 | }, 208 | "urlOverrides": { 209 | "https://github.com/araq/ormin": "https://github.com/useMyForkInstead/ormin" 210 | }, 211 | "plugins": "", 212 | } 213 | 214 | ``` 215 | 216 | The `$` has a special meaning in a pattern: 217 | 218 | | Syntax | Meaning 219 | |--------------------|-------------------------------------------------------- 220 | |``$$`` |Matches a single dollar sign. 221 | |``$*`` |Matches until the token following the ``$*`` was found. 222 | | |The match is allowed to be of 0 length. 223 | |``$+`` |Matches until the token following the ``$+`` was found. 224 | | |The match must consist of at least one char. 225 | |``$s`` |Skips optional whitespace. 226 | 227 | For example, here is how to override any github link: 228 | 229 | ```json 230 | "urlOverrides": { 231 | "https://github.com/$+": "https://utopia.forall/$#" 232 | } 233 | ``` 234 | 235 | You can use `$1` or `$#` to refer to captures. 236 | 237 | 238 | ## Virtual Nim environments 239 | 240 | Atlas supports setting up a virtual Nim environment via the `env` command. You can 241 | even install multiple different Nim versions into the same project. 242 | 243 | For example: 244 | 245 | ``` 246 | atlas env 1.6.12 247 | atlas env devel 248 | ``` 249 | 250 | When completed, run `source nim-1.6.12/activate.sh` on UNIX and `nim-1.6.12/activate.bat` on Windows. 251 | 252 | 253 | ## Dependency resolution 254 | 255 | To change the used dependency resolution mechanism, edit the `resolver` value of 256 | your `atlas.config` file. The possible values are: 257 | 258 | ### MaxVer 259 | 260 | The default resolution mechanism is called "MaxVer" where the highest available version is selected 261 | that still fits the requirements. 262 | 263 | Suppose you have a dependency called "mylibrary" with the following available versions: 264 | 1.0.0, 1.1.0, and 2.0.0. `MaxVer` selects the version 2.0.0. 265 | 266 | 267 | 268 | ### SemVer 269 | 270 | Adhere to Semantic Versioning (SemVer) by selecting the highest version that satisfies the specified 271 | version range. SemVer follows the format of `MAJOR.MINOR.PATCH`, where: 272 | 273 | MAJOR version indicates incompatible changes. 274 | 275 | MINOR version indicates backward-compatible new features. 276 | 277 | PATCH version indicates backward-compatible bug fixes. 278 | 279 | Consider the same "mylibrary" dependency with versions 1.0.0, 1.1.0, and 2.0.0. If you set the 280 | resolver to `SemVer` and specify a version range requirement of `>= 1.0.0`, the highest version 281 | that satisfies the range that does not introduce incompatible changes will be selected. In this 282 | case, the selected version would be 1.1.0. 283 | 284 | 285 | ### MinVer 286 | 287 | For the "mylibrary" dependency with versions 1.0.0, 1.1.0, and 2.0.0, if you set the resolver 288 | to `MinVer` and specify multiple minimum versions, the highest version among the minimum 289 | required versions will be selected. For example, if you specify a minimum requirement of 290 | both `>=1.0.0` and `>=2.0.0`, the selected version would be 2.0.0. 291 | 292 | 293 | ## Reproducible builds / lockfiles 294 | 295 | Atlas supports lockfiles for reproducible builds via its `pin` and `rep` commands. 296 | 297 | **Notice**: Atlas helps with reproducible builds, but it is not a complete solution. 298 | For a truely reproducible build you also need to pin the used C++ compiler, any 299 | third party dependencies ("libc" etc.) and the version of your operating system. 300 | 301 | 302 | ### pin [atlas.lock] 303 | 304 | `atlas pin` can be run either in the project or in a specific project. It "pins" the used 305 | repositories to their current commit hashes. 306 | If run in the project the entire project is "pinned" in the `atlas.lock` file. 307 | If run in a project the project's dependencies but not the project itself is "pinned" in the 308 | lock file. 309 | 310 | ### rep [atlas.lock] 311 | 312 | The `rep` command replays or repeats the projects to use the pinned commit hashes. If the 313 | projects have any "build" instructions these are performed too unless the `--noexec` switch 314 | is used. 315 | 316 | 317 | ## Plugins 318 | 319 | Atlas operates on a graph of dependencies. A dependency is a git project of a specific commit. 320 | The graph and version selection algorithms are mostly programming language agnostic. Thus it is 321 | easy to integrate foreign projects as dependencies into your project. 322 | 323 | This is accomplished by Atlas plugins. A plugin is a NimScript snippet that can call into 324 | external tools via `exec`. 325 | 326 | To enable plugins, add the line `plugins="_plugins"` to your `atlas.config` file. Then create 327 | a directory `_plugins` in your project. Every `*.nims` file inside the plugins directory is 328 | integrated into Atlas. 329 | 330 | 331 | ### Builders 332 | 333 | A builder is a build tool like `make` or `cmake`. What tool to use is determined by the existence 334 | of certain files in the project's top level directory. For example, a file `CMakeLists.txt` 335 | indicates a `cmake` based build: 336 | 337 | ```nim 338 | 339 | builder "CMakeLists.txt": 340 | mkDir "build" 341 | withDir "build": 342 | exec "cmake .." 343 | exec "cmake --build . --config Release" 344 | 345 | ``` 346 | 347 | Save this as `_plugins/cmake.nims`. Then every dependency that contains a `CMakeLists.txt` file 348 | will be build with `cmake`. 349 | 350 | **Note**: To disable any kind of action that might run arbitrary code, use the `--noexec` switch. 351 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 The Nim programming language 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 | -------------------------------------------------------------------------------- /plugins/cmake.nims: -------------------------------------------------------------------------------- 1 | # Plugin for overly simplistic cmake integration 2 | 3 | builder "CMakeLists.txt": 4 | mkDir "build" 5 | withDir "build": 6 | exec "cmake .." 7 | exec "cmake --build . --config Release" 8 | -------------------------------------------------------------------------------- /plugins/scons.nims: -------------------------------------------------------------------------------- 1 | # Plugin for overly simplistic scons integration 2 | 3 | builder "SConstruct": 4 | exec "scons" 5 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # atlas 2 | The Atlas Package cloner. It manages project dependencies in an isolated `deps/` directory. 3 | 4 | # Installation 5 | 6 | Upcoming Nim version 2.0 will ship with `atlas`. Building from source: 7 | 8 | ```sh 9 | git clone https://github.com/nim-lang/atlas.git 10 | cd atlas 11 | nim c src/atlas.nim 12 | # copy src/atlas[.exe] somewhere in your PATH 13 | ``` 14 | 15 | # Documentation 16 | 17 | Read the [full documentation](./doc/atlas.md) or go through the following tutorial. 18 | 19 | ## Tutorial 20 | 21 | Create a new project. A project contains everything we need and can safely be deleted after 22 | this tutorial: 23 | 24 | ```sh 25 | mkdir project 26 | cd project 27 | atlas init 28 | ``` 29 | 30 | Create a new project inside the project: 31 | 32 | ```sh 33 | mkdir myproject 34 | cd myproject 35 | ``` 36 | 37 | Tell Atlas we want to use the "malebolgia" library: 38 | 39 | ```sh 40 | atlas use malebolgia 41 | ``` 42 | 43 | Now `import malebolgia` in your Nim code and run the compiler as usual: 44 | 45 | ```sh 46 | echo "import malebolgia" >myproject.nim 47 | nim c myproject.nim 48 | ``` 49 | 50 | The project structure looks like this: 51 | 52 | ``` 53 | $project / project.nimble 54 | $project / nim.cfg 55 | $project / other main project files... 56 | $project / deps / atlas.config 57 | $project / deps / dependency-A 58 | $project / deps / dependency-B 59 | $project / deps / dependency-C.nimble-link (for linked projects) 60 | ``` 61 | 62 | ## Using URLs and local folders 63 | 64 | ```sh 65 | atlas use https://github.com/zedeus/nitter 66 | atlas link ../../existingDepdency/ 67 | ``` 68 | 69 | ## Debugging 70 | 71 | Sometimes it's helpful to understand what Atlas is doing. You can run commands with: `atlas --verbosity:` to get more information. 72 | 73 | ## Installing Nim with Atlas 74 | 75 | ```sh 76 | atlas env 2.0.0 77 | source deps/nim-2.0.0/activate.sh 78 | ``` 79 | 80 | ## Dependencies 81 | 82 | Atlas places dependencies in a `deps/` directory. This is especially helpful for working with projects that have dependencies pinned as git submodules, which was common in the pre-Atlas era. 83 | 84 | The `deps/` directory contains: 85 | - `atlas.config`: Configuration file for dependency management 86 | - Individual dependency directories 87 | - `nimble-link` files for linked projects 88 | 89 | Note that `atlas.config` file can be placed in the main project directory as well. In this case, the dependencies directory can modified by setting the `deps` field. 90 | -------------------------------------------------------------------------------- /src/atlas.nim.cfg: -------------------------------------------------------------------------------- 1 | --define:ssl 2 | --path:"$nim" 3 | -------------------------------------------------------------------------------- /src/basic/compiledpatterns.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Atlas Package Cloner 3 | # (c) Copyright 2021 Andreas Rumpf 4 | # 5 | # See the file "copying.txt", included in this 6 | # distribution, for details about the copyright. 7 | # 8 | 9 | ##[ 10 | 11 | Syntax taken from strscans.nim: 12 | 13 | ================= ======================================================== 14 | ``$$`` Matches a single dollar sign. 15 | ``$*`` Matches until the token following the ``$*`` was found. 16 | The match is allowed to be of 0 length. 17 | ``$+`` Matches until the token following the ``$+`` was found. 18 | The match must consist of at least one char. 19 | ``$s`` Skips optional whitespace. 20 | ================= ======================================================== 21 | 22 | ]## 23 | 24 | import tables 25 | from strutils import continuesWith, Whitespace 26 | 27 | type 28 | Opcode = enum 29 | MatchVerbatim # needs verbatim match 30 | Capture0Until 31 | Capture1Until 32 | Capture0UntilEnd 33 | Capture1UntilEnd 34 | SkipWhitespace 35 | 36 | Instr = object 37 | opc: Opcode 38 | arg1: uint8 39 | arg2: uint16 40 | 41 | Pattern* = object 42 | code: seq[Instr] 43 | usedMatches: int 44 | error: string 45 | 46 | # A rewrite rule looks like: 47 | # 48 | # foo$*bar -> https://gitlab.cross.de/$1 49 | 50 | proc compile*(pattern: string; strings: var seq[string]): Pattern = 51 | proc parseSuffix(s: string; start: int): int = 52 | result = start 53 | while result < s.len and s[result] != '$': 54 | inc result 55 | 56 | result = Pattern(code: @[], usedMatches: 0, error: "") 57 | var p = 0 58 | while p < pattern.len: 59 | if pattern[p] == '$' and p+1 < pattern.len: 60 | case pattern[p+1] 61 | of '$': 62 | if result.code.len > 0 and result.code[^1].opc in { 63 | MatchVerbatim, Capture0Until, Capture1Until, Capture0UntilEnd, Capture1UntilEnd}: 64 | # merge with previous opcode 65 | let key = strings[result.code[^1].arg2] & "$" 66 | var idx = find(strings, key) 67 | if idx < 0: 68 | idx = strings.len 69 | strings.add key 70 | result.code[^1].arg2 = uint16(idx) 71 | else: 72 | var idx = find(strings, "$") 73 | if idx < 0: 74 | idx = strings.len 75 | strings.add "$" 76 | result.code.add Instr(opc: MatchVerbatim, 77 | arg1: uint8(0), arg2: uint16(idx)) 78 | inc p, 2 79 | of '+', '*': 80 | let isPlus = pattern[p+1] == '+' 81 | 82 | let pEnd = parseSuffix(pattern, p+2) 83 | let suffix = pattern.substr(p+2, pEnd-1) 84 | p = pEnd 85 | if suffix.len == 0: 86 | result.code.add Instr(opc: if isPlus: Capture1UntilEnd else: Capture0UntilEnd, 87 | arg1: uint8(result.usedMatches), arg2: uint16(0)) 88 | else: 89 | var idx = find(strings, suffix) 90 | if idx < 0: 91 | idx = strings.len 92 | strings.add suffix 93 | result.code.add Instr(opc: if isPlus: Capture1Until else: Capture0Until, 94 | arg1: uint8(result.usedMatches), arg2: uint16(idx)) 95 | inc result.usedMatches 96 | 97 | of 's': 98 | result.code.add Instr(opc: SkipWhitespace) 99 | inc p, 2 100 | else: 101 | result.error = "unknown syntax '$" & pattern[p+1] & "'" 102 | break 103 | elif pattern[p] == '$': 104 | result.error = "unescaped '$'" 105 | break 106 | else: 107 | let pEnd = parseSuffix(pattern, p) 108 | let suffix = pattern.substr(p, pEnd-1) 109 | var idx = find(strings, suffix) 110 | if idx < 0: 111 | idx = strings.len 112 | strings.add suffix 113 | result.code.add Instr(opc: MatchVerbatim, 114 | arg1: uint8(0), arg2: uint16(idx)) 115 | p = pEnd 116 | 117 | type 118 | MatchObj = object 119 | m: int 120 | a: array[20, (int, int)] 121 | 122 | proc matches(s: Pattern; strings: seq[string]; input: string): MatchObj = 123 | template failed = 124 | result.m = -1 125 | return result 126 | 127 | var i = 0 128 | for instr in s.code: 129 | case instr.opc 130 | of MatchVerbatim: 131 | if continuesWith(input, strings[instr.arg2], i): 132 | inc i, strings[instr.arg2].len 133 | else: 134 | failed() 135 | of Capture0Until, Capture1Until: 136 | block searchLoop: 137 | let start = i 138 | while i < input.len: 139 | if continuesWith(input, strings[instr.arg2], i): 140 | if instr.opc == Capture1Until and i == start: 141 | failed() 142 | result.a[result.m] = (start, i-1) 143 | inc result.m 144 | inc i, strings[instr.arg2].len 145 | break searchLoop 146 | inc i 147 | failed() 148 | 149 | of Capture0UntilEnd, Capture1UntilEnd: 150 | if instr.opc == Capture1UntilEnd and i >= input.len: 151 | failed() 152 | result.a[result.m] = (i, input.len-1) 153 | inc result.m 154 | i = input.len 155 | of SkipWhitespace: 156 | while i < input.len and input[i] in Whitespace: inc i 157 | if i < input.len: 158 | # still unmatched stuff was left: 159 | failed() 160 | 161 | proc translate(m: MatchObj; outputPattern, input: string): string = 162 | result = newStringOfCap(outputPattern.len) 163 | var i = 0 164 | var patternCount = 0 165 | while i < outputPattern.len: 166 | if i+1 < outputPattern.len and outputPattern[i] == '$': 167 | if outputPattern[i+1] == '#': 168 | inc i, 2 169 | if patternCount < m.a.len: 170 | let (a, b) = m.a[patternCount] 171 | for j in a..b: result.add input[j] 172 | inc patternCount 173 | elif outputPattern[i+1] in {'1'..'9'}: 174 | var n = ord(outputPattern[i+1]) - ord('0') 175 | inc i, 2 176 | while i < outputPattern.len and outputPattern[i] in {'0'..'9'}: 177 | n = n * 10 + (ord(outputPattern[i]) - ord('0')) 178 | inc i 179 | patternCount = n 180 | if n-1 < m.a.len: 181 | let (a, b) = m.a[n-1] 182 | for j in a..b: result.add input[j] 183 | else: 184 | # just ignore the wrong pattern: 185 | inc i 186 | else: 187 | result.add outputPattern[i] 188 | inc i 189 | 190 | proc replace*(s: Pattern; outputPattern, input: string): string = 191 | var strings: seq[string] = @[] 192 | let m = s.matches(strings, input) 193 | if m.m < 0: 194 | result = "" 195 | else: 196 | result = translate(m, outputPattern, input) 197 | 198 | 199 | type 200 | Patterns* = object 201 | s: seq[(Pattern, string)] 202 | t: Table[string, string] 203 | strings: seq[string] 204 | patterns: seq[(string, string)] # Store original (input, output) pattern pairs 205 | 206 | proc initPatterns*(): Patterns = 207 | Patterns(s: @[], t: initTable[string, string](), strings: @[], patterns: @[]) 208 | 209 | proc addPattern*(p: var Patterns; inputPattern, outputPattern: string): string = 210 | if '$' notin inputPattern and '$' notin outputPattern: 211 | p.t[inputPattern] = outputPattern 212 | p.patterns.add((inputPattern, outputPattern)) 213 | result = "" 214 | else: 215 | let code = compile(inputPattern, p.strings) 216 | if code.error.len > 0: 217 | result = code.error 218 | else: 219 | p.s.add (code, outputPattern) 220 | p.patterns.add((inputPattern, outputPattern)) 221 | result = "" 222 | 223 | proc substitute*(p: Patterns; input: string; didReplace: var bool): string = 224 | if input in p.t: 225 | didReplace = true 226 | result = p.t[input] 227 | if result.len == 0: 228 | for i in 0..= 0: 231 | didReplace = true 232 | return translate(m, p.s[i][1], input) 233 | return input 234 | 235 | proc substitute*(p: Patterns; input: string): string = 236 | var ignored = false 237 | substitute(p, input, ignored) 238 | 239 | proc replacePattern*(inputPattern, outputPattern, input: string): string = 240 | var strings: seq[string] = @[] 241 | let code = compile(inputPattern, strings) 242 | result = replace(code, outputPattern, input) 243 | 244 | proc toTable*(p: Patterns): Table[string, string] = 245 | ## Convert the patterns back to a table for serialization 246 | result = initTable[string, string]() 247 | for (input, output) in p.patterns: 248 | result[input] = output 249 | 250 | when isMainModule: 251 | # foo$*bar -> https://gitlab.cross.de/$1 252 | const realInput = "$fooXXbar$z00end" 253 | var strings: seq[string] = @[] 254 | let code = compile("$$foo$*bar$$$*z00$*", strings) 255 | echo code 256 | 257 | let m = code.matches(strings, realInput) 258 | echo m.m 259 | 260 | echo translate(m, "$1--$#-$#-", realInput) 261 | 262 | echo translate(m, "https://gitlab.cross.de/$1", realInput) 263 | 264 | var pat = initPatterns() 265 | discard pat.addPattern("$+", "file://./source/$#") 266 | echo "proj_a: ", substitute(pat, "proj_a") 267 | 268 | var pat2 = initPatterns() 269 | discard pat2.addPattern("proj$+", "file://./source/proj$#") 270 | echo "proj_a: ", substitute(pat2, "proj_a") 271 | 272 | var pat3 = initPatterns() 273 | discard pat3.addPattern("proj_a", "file://./source/proj_a") 274 | echo "proj_a: ", substitute(pat3, "proj_a") -------------------------------------------------------------------------------- /src/basic/compilerversions.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Atlas Package Cloner 3 | # (c) Copyright 2023 Andreas Rumpf 4 | # 5 | # See the file "copying.txt", included in this 6 | # distribution, for details about the copyright. 7 | # 8 | 9 | ## Extract the version of commonly used compilers. 10 | ## For Nim we use the version plus the commit hash. 11 | 12 | import std / [osproc, strscans] 13 | 14 | proc detectGccVersion*(): string = 15 | result = "" 16 | let (outp, exitCode) = execCmdEx("gcc -v") 17 | if exitCode == 0: 18 | var prefix: string 19 | var a, b, c: int 20 | if scanf(outp, "$*\ngcc version $i.$i.$i", prefix, a, b, c): 21 | result = $a & "." & $b & "." & $c 22 | 23 | proc detectClangVersion*(): string = 24 | result = "" 25 | let (outp, exitCode) = execCmdEx("clang -v") 26 | if exitCode == 0: 27 | var a, b, c: int 28 | if scanf(outp, "clang version $i.$i.$i", a, b, c): 29 | result = $a & "." & $b & "." & $c 30 | 31 | proc detectNimVersion*(): string = 32 | result = "" 33 | let (outp, exitCode) = execCmdEx("nim -v") 34 | if exitCode == 0: 35 | var a, b, c: int 36 | if scanf(outp, "Nim Compiler Version $i.$i.$i", a, b, c): 37 | result = $a & "." & $b & "." & $c 38 | var prefix, commit: string 39 | if scanf(outp, "$*\ngit hash: $w", prefix, commit): 40 | result.add ' ' 41 | result.add commit 42 | -------------------------------------------------------------------------------- /src/basic/configutils.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Atlas Package Cloner 3 | # (c) Copyright 2021 Andreas Rumpf 4 | # 5 | # See the file "copying.txt", included in this 6 | # distribution, for details about the copyright. 7 | # 8 | 9 | import std/[os, strutils, files] 10 | import context, osutils, parse_requires, reporters 11 | 12 | export parse_requires 13 | 14 | const 15 | configPatternBegin = "############# begin Atlas config section ##########\n" 16 | configPatternEnd = "############# end Atlas config section ##########\n" 17 | 18 | proc findCfgDir*(dir: Path): CfgPath = 19 | for nimbleFile in walkPattern($dir / "*.nimble"): 20 | let nimbleInfo = extractRequiresInfo(Path nimbleFile) 21 | return CfgPath dir / nimbleInfo.srcDir 22 | return CfgPath dir 23 | 24 | proc patchNimCfg*(deps: seq[CfgPath]; cfgPath: CfgPath) = 25 | var paths = "--noNimblePath\n" 26 | for d in deps: 27 | let x = relativePath(d.string, cfgPath.string, '/') 28 | if x.len > 0 and x != ".": 29 | paths.add "--path:\"" & x & "\"\n" 30 | var cfgContent = configPatternBegin & paths & configPatternEnd 31 | 32 | let cfg = Path(cfgPath.string / "nim.cfg") 33 | assert cfgPath.string.len > 0 34 | if cfgPath.string.len > 0 and not dirExists(cfgPath.string): 35 | error($project(), "could not write the nim.cfg") 36 | elif not fileExists(cfg): 37 | writeFile($cfg, cfgContent) 38 | info(project(), "created: " & $cfg.readableFile(project())) 39 | else: 40 | let content = readFile($cfg) 41 | let start = content.find(configPatternBegin) 42 | if start >= 0: 43 | cfgContent = content.substr(0, start-1) & cfgContent 44 | let theEnd = content.find(configPatternEnd, start) 45 | if theEnd >= 0: 46 | cfgContent.add content.substr(theEnd+len(configPatternEnd)) 47 | else: 48 | cfgContent = content & "\n" & cfgContent 49 | if cfgContent != content: 50 | # do not touch the file if nothing changed 51 | # (preserves the file date information): 52 | writeFile($cfg, cfgContent) 53 | info(project(), "updated: " & $cfg.readableFile(project())) 54 | -------------------------------------------------------------------------------- /src/basic/context.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Atlas Package Cloner 3 | # (c) Copyright 2023 Andreas Rumpf 4 | # 5 | # See the file "copying.txt", included in this 6 | # distribution, for details about the copyright. 7 | # 8 | 9 | import std / [os, uri, paths, files, tables, sets] 10 | import versions, parse_requires, compiledpatterns, reporters 11 | 12 | export reporters 13 | 14 | const 15 | UnitTests* = defined(atlasUnitTests) 16 | TestsDir* = "atlas/tests" 17 | 18 | const 19 | AtlasProjectConfig = Path"atlas.config" 20 | DefaultPackagesSubDir* = Path"_packages" 21 | DefaultCachesSubDir* = Path"_caches" 22 | DefaultNimbleCachesSubDir* = Path"_nimbles" 23 | 24 | 25 | type 26 | CfgPath* = distinct string # put into a config `--path:"../x"` 27 | 28 | SemVerField* = enum 29 | major, minor, patch 30 | 31 | CloneStatus* = enum 32 | Ok, NotFound, OtherError 33 | 34 | Flag* = enum 35 | KeepCommits 36 | CfgHere 37 | KeepNimEnv 38 | KeepWorkspace 39 | ShowGraph 40 | AutoEnv 41 | NoExec 42 | UpdateRepos 43 | ListVersions 44 | ListVersionsOff 45 | GlobalWorkspace 46 | ShallowClones 47 | IgnoreGitRemoteUrls 48 | IgnoreErrors 49 | DumpFormular 50 | DumpGraphs 51 | DumbProxy 52 | ForceGitToHttps 53 | IncludeTagsAndNimbleCommits # include nimble commits and tags in the solver 54 | NimbleCommitsMax # takes the newest commit for each version 55 | 56 | AtlasContext* = object 57 | projectDir*: Path = Path"." 58 | depsDir*: Path = Path"deps" 59 | flags*: set[Flag] = {} 60 | nameOverrides*: Patterns 61 | urlOverrides*: Patterns 62 | pkgOverrides*: Table[string, Uri] 63 | defaultAlgo*: ResolutionAlgorithm = SemVer 64 | plugins*: PluginInfo 65 | overridesFile*: Path 66 | pluginsFile*: Path 67 | proxy*: Uri 68 | features*: HashSet[string] 69 | 70 | var atlasContext = AtlasContext() 71 | 72 | proc setContext*(ctx: AtlasContext) = 73 | atlasContext = ctx 74 | proc context*(): var AtlasContext = 75 | atlasContext 76 | 77 | proc project*(): Path = 78 | atlasContext.projectDir 79 | 80 | proc project*(ws: Path) = 81 | atlasContext.projectDir = ws 82 | 83 | proc depsDir*(relative = false): Path = 84 | if atlasContext.depsDir == Path"": 85 | result = Path"" 86 | elif relative or atlasContext.depsDir.isAbsolute: 87 | result = atlasContext.depsDir 88 | else: 89 | result = atlasContext.projectDir / atlasContext.depsDir 90 | 91 | proc packagesDirectory*(): Path = 92 | depsDir() / DefaultPackagesSubDir 93 | 94 | proc cachesDirectory*(): Path = 95 | depsDir() / DefaultCachesSubDir 96 | 97 | proc nimbleCachesDirectory*(): Path = 98 | depsDir() / DefaultNimbleCachesSubDir 99 | 100 | proc depGraphCacheFile*(ctx: AtlasContext): Path = 101 | ctx.projectDir / ctx.depsDir / Path"atlas.cache.json" 102 | 103 | proc relativeToWorkspace*(path: Path): string = 104 | result = "$project/" & $path.relativePath(project()) 105 | 106 | proc getProjectConfig*(dir = project()): Path = 107 | ## prefer project atlas.config if found 108 | ## otherwise default to one in deps/ 109 | ## the deps path will be the default for auto-created ones 110 | result = dir / AtlasProjectConfig 111 | if fileExists(result): return 112 | result = depsDir() / AtlasProjectConfig 113 | 114 | proc isProject*(dir: Path): bool = 115 | fileExists(getProjectConfig(dir)) 116 | 117 | proc `==`*(a, b: CfgPath): bool {.borrow.} 118 | 119 | proc displayName(c: AtlasContext; p: string): string = 120 | if p == c.projectDir.string: 121 | p.absolutePath 122 | elif $c.depsDir != "" and p.isRelativeTo($c.depsDir): 123 | p.relativePath($c.depsDir) 124 | elif p.isRelativeTo($c.projectDir): 125 | p.relativePath($c.projectDir) 126 | else: 127 | p 128 | -------------------------------------------------------------------------------- /src/basic/depgraphtypes.nim: -------------------------------------------------------------------------------- 1 | import std / [paths, tables, streams, json, jsonutils] 2 | 3 | import sattypes, context, deptypes, reporters, nimblecontext, pkgurls, versions 4 | 5 | 6 | type 7 | VisitState = enum 8 | NotVisited, InProgress, Visited 9 | 10 | proc toposorted*(graph: DepGraph): seq[Package] = 11 | ## Returns a sequence of packages in topological order 12 | ## Packages that are depended upon come before packages that depend on them 13 | result = @[] 14 | var visited = initTable[PkgUrl, VisitState]() 15 | 16 | # Initialize all packages as not visited 17 | for url, pkg in graph.pkgs: 18 | visited[url] = NotVisited 19 | 20 | # DFS-based topological sort 21 | proc visit(pkg: Package): seq[Package] = 22 | if visited[pkg.url] == Visited: 23 | return 24 | if visited[pkg.url] == InProgress: 25 | # This means we have a cycle, which shouldn't happen in a valid dependency graph 26 | # But we'll handle it gracefully 27 | return 28 | 29 | visited[pkg.url] = InProgress 30 | 31 | # Get the active release to check its dependencies 32 | let release = pkg.activeNimbleRelease() 33 | if not release.isNil: 34 | # Visit all dependencies first 35 | for (depUrl, _) in release.requirements: 36 | if depUrl in graph.pkgs: 37 | let depPkg = graph.pkgs[depUrl] 38 | result.add visit(depPkg) 39 | 40 | # Mark as visited and add to result 41 | visited[pkg.url] = Visited 42 | result.add(pkg) 43 | 44 | # Start with root package 45 | if not graph.root.isNil: 46 | result.add visit(graph.root) 47 | 48 | # Visit any remaining packages (disconnected or not reachable from root) 49 | for url, pkg in graph.pkgs: 50 | if visited[url] == NotVisited: 51 | result.add visit(pkg) 52 | 53 | proc validateDependencyGraph*(graph: DepGraph): bool = 54 | ## Checks if the dependency graph is valid (no cycles) 55 | var visited = initTable[PkgUrl, VisitState]() 56 | 57 | # Initialize all packages as not visited 58 | for url, pkg in graph.pkgs: 59 | visited[url] = NotVisited 60 | 61 | proc checkCycles(pkg: Package): bool = 62 | if visited[pkg.url] == Visited: 63 | return true 64 | if visited[pkg.url] == InProgress: 65 | # Cycle detected 66 | return false 67 | 68 | visited[pkg.url] = InProgress 69 | 70 | # Check all dependencies 71 | let release = pkg.activeNimbleRelease() 72 | if not release.isNil: 73 | for (depUrl, _) in release.requirements: 74 | if depUrl in graph.pkgs: 75 | let depPkg = graph.pkgs[depUrl] 76 | if not checkCycles(depPkg): 77 | return false 78 | 79 | visited[pkg.url] = Visited 80 | return true 81 | 82 | # Check from all possible starting points 83 | for url, pkg in graph.pkgs: 84 | if visited[url] == NotVisited: 85 | if not checkCycles(pkg): 86 | return false 87 | 88 | return true 89 | 90 | proc toDestDir*(g: DepGraph; d: Package): Path = 91 | result = d.ondisk 92 | 93 | iterator allNodes*(g: DepGraph): Package = 94 | for pkg in values(g.pkgs): 95 | yield pkg 96 | 97 | iterator allActiveNodes*(g: DepGraph): Package = 98 | for pkg in values(g.pkgs): 99 | if pkg.active and not pkg.activeVersion.isNil: 100 | doAssert pkg.state == Processed or pkg.state == LazyDeferred 101 | yield pkg 102 | 103 | proc getCfgPath*(g: DepGraph; d: Package): lent CfgPath = 104 | result = CfgPath g.pkgs[d.url].activeNimbleRelease().srcDir 105 | 106 | proc bestNimVersion*(g: DepGraph): Version = 107 | result = Version"" 108 | for pkg in allNodes(g): 109 | if pkg.active and pkg.activeNimbleRelease().nimVersion != Version"": 110 | let v = pkg.activeNimbleRelease().nimVersion 111 | if v > result: result = v 112 | -------------------------------------------------------------------------------- /src/basic/deptypes.nim: -------------------------------------------------------------------------------- 1 | import std/[paths, tables, json, jsonutils, hashes, sets] 2 | import sattypes, pkgurls, versions, context 3 | 4 | export tables 5 | 6 | type 7 | 8 | PackageState* = enum 9 | NotInitialized 10 | LazyDeferred 11 | DoLoad 12 | Found 13 | Processed 14 | Error 15 | 16 | ReleaseStatus* = enum 17 | Normal, HasBrokenRepo, HasBrokenNimbleFile, HasBrokenRelease, HasUnknownNimbleFile, HasBrokenDep 18 | 19 | Package* = ref object 20 | url*: PkgUrl 21 | state*: PackageState 22 | versions*: OrderedTable[PackageVersion, NimbleRelease] 23 | activeVersion*: PackageVersion 24 | module*: string 25 | ondisk*: Path 26 | nimbleFile*: Path 27 | active*: bool 28 | isAtlasProject*: bool # true if the package is an atlas project 29 | isRoot*, isLocalOnly*: bool 30 | errors*: seq[string] 31 | originHead*: CommitHash 32 | 33 | NimbleRelease* = ref object 34 | version*: Version 35 | nimVersion*: Version 36 | status*: ReleaseStatus 37 | requirements*: seq[(PkgUrl, VersionInterval)] 38 | reqsByFeatures*: Table[PkgUrl, HashSet[string]] 39 | hasInstallHooks*: bool 40 | srcDir*: Path 41 | err*: string 42 | features*: Table[string, seq[(PkgUrl, VersionInterval)]] 43 | featureVars*: Table[string, VarId] 44 | 45 | PackageVersion* = ref object 46 | vtag*: VersionTag 47 | vid*: VarId = NoVar 48 | 49 | DepGraph* = object 50 | mode*: TraversalMode 51 | root*: Package 52 | pkgs*: OrderedTable[PkgUrl, Package] 53 | 54 | TraversalMode* = enum 55 | AllReleases, 56 | ExplicitVersions, 57 | CurrentCommit 58 | 59 | const 60 | EmptyReqs* = 0 61 | UnknownReqs* = 1 62 | 63 | FileWorkspace* = "file://" 64 | 65 | proc toPkgVer*(vtag: VersionTag): PackageVersion = 66 | result = PackageVersion(vtag: vtag) 67 | 68 | proc version*(pv: PackageVersion): Version = 69 | pv.vtag.version 70 | proc commit*(pv: PackageVersion): CommitHash = 71 | pv.vtag.commit 72 | 73 | proc sortVersionsDesc*(a, b: (VersionTag, NimbleRelease)): int = 74 | sortVersionsDesc(a[0], b[0]) 75 | 76 | proc sortVersionsDesc*(a, b: (PackageVersion, NimbleRelease)): int = 77 | sortVersionsDesc(a[0].vtag, b[0].vtag) 78 | 79 | proc sortVersionsAsc*(a, b: (VersionTag, NimbleRelease)): int = 80 | sortVersionsAsc(a[0], b[0]) 81 | 82 | proc sortVersionsAsc*(a, b: (PackageVersion, NimbleRelease)): int = 83 | sortVersionsAsc(a[0].vtag, b[0].vtag) 84 | 85 | proc `$`*(d: Package): string = 86 | d.url.projectName() 87 | 88 | proc projectName*(d: Package): string = 89 | d.url.projectName() 90 | 91 | proc hash*(r: Package): Hash = 92 | ## use pkg name and url for identification and lookups 93 | var h: Hash = 0 94 | h = h !& hash(r.url) 95 | result = !$h 96 | 97 | proc hash*(r: NimbleRelease): Hash = 98 | var h: Hash = 0 99 | h = h !& hash(r.version) 100 | h = h !& hash(r.requirements) 101 | h = h !& hash(r.nimVersion) 102 | h = h !& hash(r.hasInstallHooks) 103 | h = h !& hash($r.srcDir) 104 | h = h !& hash($r.err) 105 | h = h !& hash($r.status) 106 | result = !$h 107 | 108 | proc `==`*(a, b: NimbleRelease): bool = 109 | result = true 110 | result = result and a.version == b.version 111 | result = result and a.requirements == b.requirements 112 | result = result and a.nimVersion == b.nimVersion 113 | result = result and a.hasInstallHooks == b.hasInstallHooks 114 | result = result and a.srcDir == b.srcDir 115 | result = result and a.err == b.err 116 | result = result and a.status == b.status 117 | 118 | proc `$`*(r: PackageVersion): string = 119 | result = $(r.vtag) 120 | 121 | proc hash*(r: PackageVersion): Hash = 122 | result = hash(r.vtag) 123 | proc `==`*(a, b: PackageVersion): bool = 124 | result = a.vtag == b.vtag 125 | 126 | proc activeNimbleRelease*(pkg: Package): NimbleRelease = 127 | if pkg.activeVersion.isNil: 128 | result = nil 129 | else: 130 | let av = pkg.activeVersion 131 | result = pkg.versions[av] 132 | 133 | proc toReporterName*(pkg: Package): string = 134 | if pkg.isNil: "nil" 135 | else: pkg.url.fullName() 136 | 137 | proc findRelease*(pkg: Package, v: VersionInterval): NimbleRelease = 138 | for vtag, release in pkg.versions: 139 | if v.matches(vtag.vtag): 140 | return release 141 | result = nil 142 | 143 | proc matches*(v: VersionInterval, pkgVer: PackageVersion): bool = 144 | v.matches(pkgVer.vtag) 145 | -------------------------------------------------------------------------------- /src/basic/deptypesjson.nim: -------------------------------------------------------------------------------- 1 | import std/[json, jsonutils, paths, strutils, tables, sequtils, sets] 2 | import sattypes, deptypes, depgraphtypes, nimblecontext, pkgurls, versions 3 | 4 | export json, jsonutils 5 | 6 | proc toJsonHook*(v: VersionInterval): JsonNode = toJson($(v)) 7 | proc toJsonHook*(v: Version): JsonNode = toJson($v) 8 | proc toJsonHook*(v: VersionTag): JsonNode = toJson(repr(v)) 9 | 10 | proc fromJsonHook*(a: var VersionInterval; b: JsonNode; opt = Joptions()) = 11 | var err = false 12 | a = parseVersionInterval(b.getStr(), 0, err) 13 | 14 | 15 | proc fromJsonHook*(a: var Version; b: JsonNode; opt = Joptions()) = 16 | a = toVersion(b.getStr()) 17 | 18 | proc fromJsonHook*(a: var VersionTag; b: JsonNode; opt = Joptions()) = 19 | a = toVersionTag(b.getStr()) 20 | proc toJsonHook*(v: PkgUrl): JsonNode = %($(v)) 21 | 22 | proc fromJsonHook*(a: var PkgUrl; b: JsonNode; opt = Joptions()) = 23 | a = toPkgUriRaw(parseUri(b.getStr())) 24 | 25 | proc toJsonHook*(vid: VarId): JsonNode = toJson(int(vid)) 26 | 27 | proc fromJsonHook*(a: var VarId; b: JsonNode; opt = Joptions()) = 28 | a = VarId(int(b.getInt())) 29 | 30 | proc toJsonHook*(p: Path): JsonNode = toJson($(p)) 31 | 32 | proc fromJsonHook*(a: var Path; b: JsonNode; opt = Joptions()) = 33 | a = Path(b.getStr()) 34 | 35 | proc toJsonHook*(v: (PkgUrl, VersionInterval), opt: ToJsonOptions): JsonNode = 36 | result = newJObject() 37 | result["url"] = toJsonHook(v[0]) 38 | result["version"] = toJsonHook(v[1]) 39 | 40 | proc fromJsonHook*(a: var (PkgUrl, VersionInterval); b: JsonNode; opt = Joptions()) = 41 | a[0].fromJson(b["url"]) 42 | a[1].fromJson(b["version"]) 43 | 44 | proc toJsonHook*(v: Table[PkgUrl, HashSet[string]], opt: ToJsonOptions): JsonNode = 45 | result = newJObject() 46 | for k, v in v: 47 | result[$(k)] = toJson(v, opt) 48 | 49 | proc fromJsonHook*(a: var Table[PkgUrl, HashSet[string]]; b: JsonNode; opt = Joptions()) = 50 | for k, v in b: 51 | var url: PkgUrl 52 | url.fromJson(toJson(k)) 53 | var flags: HashSet[string] 54 | flags.fromJson(toJson(v)) 55 | a[url] = flags 56 | 57 | proc toJsonHook*(t: OrderedTable[PackageVersion, NimbleRelease], opt: ToJsonOptions): JsonNode = 58 | result = newJArray() 59 | for k, v in t: 60 | var tpl = newJArray() 61 | tpl.add toJson(k, opt) 62 | tpl.add toJson(v, opt) 63 | result.add tpl 64 | # result[repr(k.vtag)] = toJson(v, opt) 65 | 66 | proc fromJsonHook*(t: var OrderedTable[PackageVersion, NimbleRelease]; b: JsonNode; opt = Joptions()) = 67 | for item in b: 68 | var pv: PackageVersion 69 | pv.fromJson(item[0]) 70 | var release: NimbleRelease 71 | release.fromJson(item[1]) 72 | t[pv] = release 73 | 74 | proc toJsonHook*(t: OrderedTable[PkgUrl, Package], opt: ToJsonOptions): JsonNode = 75 | result = newJObject() 76 | for k, v in t: 77 | result[$(k)] = toJson(v, opt) 78 | 79 | proc fromJsonHook*(t: var OrderedTable[PkgUrl, Package]; b: JsonNode; opt = Joptions()) = 80 | for k, v in b: 81 | var url: PkgUrl 82 | url.fromJson(toJson(k)) 83 | var pkg: Package 84 | pkg.fromJson(v) 85 | t[url] = pkg 86 | 87 | proc toJsonHook*(r: NimbleRelease, opt: ToJsonOptions = ToJsonOptions()): JsonNode = 88 | if r == nil: 89 | return newJNull() 90 | result = newJObject() 91 | result["requirements"] = toJson(r.requirements, opt) 92 | if r.hasInstallHooks: 93 | result["hasInstallHooks"] = toJson(r.hasInstallHooks, opt) 94 | if r.srcDir != Path "": 95 | result["srcDir"] = toJson(r.srcDir, opt) 96 | result["version"] = toJson(r.version, opt) 97 | result["status"] = toJson(r.status, opt) 98 | if r.err != "": 99 | result["err"] = toJson(r.err, opt) 100 | 101 | proc fromJsonHook*(r: var NimbleRelease; b: JsonNode; opt = Joptions()) = 102 | if r.isNil: 103 | r = new(NimbleRelease) 104 | r.version.fromJson(b["version"]) 105 | r.requirements.fromJson(b["requirements"]) 106 | r.status.fromJson(b["status"]) 107 | if b.hasKey("hasInstallHooks"): 108 | r.hasInstallHooks = b["hasInstallHooks"].getBool() 109 | if b.hasKey("srcDir"): 110 | r.srcDir.fromJson(b["srcDir"]) 111 | if b.hasKey("err"): 112 | r.err = b["err"].getStr() 113 | 114 | proc toJsonGraph*(d: DepGraph): JsonNode = 115 | result = toJson(d, ToJsonOptions(enumMode: joptEnumString)) 116 | 117 | proc dumpJson*(d: DepGraph, filename: string, pretty = true) = 118 | let jn = toJsonGraph(d) 119 | if pretty: 120 | writeFile(filename, pretty(jn)) 121 | else: 122 | writeFile(filename, $(jn)) 123 | 124 | proc loadJson*(nc: var NimbleContext, json: JsonNode): DepGraph = 125 | result.fromJson(json, Joptions(allowMissingKeys: true, allowExtraKeys: true)) 126 | var pkgs = result.pkgs 127 | result.pkgs.clear() 128 | 129 | for url, pkg in pkgs: 130 | let url2 = nc.createUrl($pkg.url) 131 | pkg.url = url2 132 | result.pkgs[url2] = pkg 133 | 134 | let rootUrl = nc.createUrl($result.root.url) 135 | result.root = result.pkgs[rootUrl] 136 | 137 | proc loadJson*(nc: var NimbleContext, filename: string): DepGraph = 138 | let jn = parseFile(filename) 139 | result = loadJson(nc, jn) 140 | -------------------------------------------------------------------------------- /src/basic/lockfiletypes.nim: -------------------------------------------------------------------------------- 1 | import std / [strutils, tables, json, jsonutils] 2 | export tables 3 | 4 | import reporters 5 | 6 | type 7 | LockFileEntry* = object 8 | dir*: Path 9 | url*: string 10 | commit*: string 11 | version*: string 12 | 13 | LockedNimbleFile* = object 14 | filename*: Path 15 | content*: seq[string] 16 | 17 | LockFile* = object # serialized as JSON 18 | items*: OrderedTable[string, LockFileEntry] 19 | nimcfg*: seq[string] 20 | nimbleFile*: LockedNimbleFile 21 | hostOS*, hostCPU*: string 22 | nimVersion*, gccVersion*, clangVersion*: string 23 | 24 | proc convertKeyToArray(jsonTree: var JsonNode, path: varargs[string]) = 25 | var parent: JsonNode 26 | var content: JsonNode = jsonTree 27 | for key in path: 28 | if content.hasKey(key): 29 | parent = content 30 | content = parent[key] 31 | else: 32 | return 33 | 34 | if content.kind == JString: 35 | var contents = newJArray() 36 | for line in content.getStr.split("\n"): 37 | contents.add(% line) 38 | parent[path[^1]] = contents 39 | 40 | proc readLockFile*(filename: Path): LockFile = 41 | let jsonAsStr = readFile($filename) 42 | var jsonTree = parseJson(jsonAsStr) 43 | 44 | # convert older non-array file contents to JArray 45 | jsonTree.convertKeyToArray("nimcfg") 46 | jsonTree.convertKeyToArray("nimbleFile", "content") 47 | result = jsonTo(jsonTree, LockFile, 48 | Joptions(allowExtraKeys: true, allowMissingKeys: true)) 49 | 50 | proc write*(lock: LockFile; lockFilePath: string) = 51 | writeFile lockFilePath, toJson(lock).pretty 52 | -------------------------------------------------------------------------------- /src/basic/nimblechecksums.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Atlas Package Cloner 3 | # (c) Copyright 2023 Andreas Rumpf 4 | # 5 | # See the file "copying.txt", included in this 6 | # distribution, for details about the copyright. 7 | # 8 | 9 | import std / [strutils, os, sha1, algorithm] 10 | import context, osutils 11 | import gitops 12 | 13 | proc updateSecureHash(checksum: var Sha1State; name, path: string) = 14 | if not path.fileExists(): return 15 | checksum.update(name) 16 | 17 | if symlinkExists(path): 18 | # checksum file path (?) 19 | try: 20 | let path = expandSymlink(path) 21 | checksum.update(path) 22 | except OSError: 23 | error name, "cannot follow symbolic link " & path 24 | else: 25 | # checksum file contents 26 | var file: File 27 | try: 28 | file = path.open(fmRead) 29 | const bufferSize = 8192 30 | var buffer = newString(bufferSize) 31 | while true: 32 | let bytesRead = readChars(file, buffer) 33 | if bytesRead == 0: break 34 | checksum.update(buffer.toOpenArray(0, bytesRead - 1)) 35 | except IOError: 36 | error name, "error opening file " & path 37 | finally: 38 | file.close() 39 | 40 | proc nimbleChecksum*(name: string, path: Path): string = 41 | ## calculate a nimble style checksum from a `path`. 42 | ## 43 | ## Useful for exporting a Nimble sync file. 44 | ## 45 | withDir path: 46 | var files = listFiles(path) 47 | if files.len == 0: 48 | error path, "couldn't list files" 49 | else: 50 | sort(files) 51 | var checksum = newSha1State() 52 | for file in files: 53 | checksum.updateSecureHash(name, file) 54 | result = toLowerAscii($SecureHash(checksum.finalize())) 55 | -------------------------------------------------------------------------------- /src/basic/nimblecontext.nim: -------------------------------------------------------------------------------- 1 | import std/[paths, tables, files, os, uri, dirs, sets, strutils, unicode] 2 | import context, packageinfos, reporters, pkgurls, gitops, compiledpatterns, deptypes, versions 3 | 4 | type 5 | NimbleContext* = object 6 | packageToDependency*: Table[PkgUrl, Package] 7 | packageExtras*: Table[string, PkgUrl] 8 | nameToUrl: Table[string, PkgUrl] 9 | explicitVersions*: Table[PkgUrl, HashSet[VersionTag]] 10 | nameOverrides*: Patterns 11 | urlOverrides*: Patterns 12 | hasPackageList*: bool 13 | notFoundNames: HashSet[string] 14 | 15 | proc findNimbleFile*(dir: Path, projectName: string = ""): seq[Path] = 16 | if dir.splitFile().ext == "nimble": 17 | let nimbleFile = dir 18 | if fileExists(nimbleFile): 19 | return @[nimbleFile] 20 | else: 21 | let nimbleFile = dir / Path(projectName & ".nimble") 22 | if fileExists(nimbleFile): 23 | return @[nimbleFile] 24 | 25 | if result.len() == 0: 26 | for file in walkFiles($dir / "*.nimble"): 27 | result.add Path(file) 28 | debug dir, "finding nimble file searching by name:", projectName, "found:", result.join(", ") 29 | 30 | proc findNimbleFile*(info: Package): seq[Path] = 31 | doAssert(info.ondisk.string != "", "Package ondisk must be set before findNimbleFile can be called! Package: " & $(info)) 32 | result = findNimbleFile(info.ondisk, info.projectName() & ".nimble") 33 | 34 | proc cacheNimbleFilesFromGit*(pkg: Package, commit: CommitHash): seq[Path] = 35 | # First check if we already have cached nimble files for this commit 36 | for file in walkFiles($nimbleCachesDirectory() / (commit.h & "-*.nimble")): 37 | let path = Path(file) 38 | let nimbleFile = path.splitPath().tail 39 | if nimbleFile == Path(commit.h & "-" & pkg.url.projectName & ".nimble"): 40 | return @[path] 41 | result.add path 42 | 43 | if result.len > 0: 44 | return result 45 | 46 | let files = listFiles(pkg.ondisk, commit) 47 | var nimbleFiles: seq[Path] 48 | for file in files: 49 | if file.endsWith(".nimble"): 50 | if file == pkg.url.projectName & ".nimble": 51 | nimbleFiles = @[Path(file)] 52 | break 53 | nimbleFiles.add Path(file) 54 | 55 | createDir(nimbleCachesDirectory()) 56 | for nimbleFile in nimbleFiles: 57 | let cachePath = nimbleCachesDirectory() / Path(commit.h & "-" & $nimbleFile.splitPath().tail) 58 | if not fileExists(cachePath): 59 | let contents = showFile(pkg.ondisk, commit, $nimbleFile) 60 | writeFile($cachePath, contents) 61 | result.add cachePath 62 | 63 | proc lookup*(nc: NimbleContext, name: string): PkgUrl = 64 | let lname = unicode.toLower(name) 65 | if lname in nc.packageExtras: 66 | result = nc.packageExtras[lname] 67 | elif lname in nc.nameToUrl: 68 | result = nc.nameToUrl[lname] 69 | 70 | proc putImpl(nc: var NimbleContext, name: string, url: PkgUrl, isFromPath = false): bool = 71 | let name = unicode.toLower(name) 72 | if name in nc.nameToUrl: 73 | result = false 74 | elif name notin nc.packageExtras: 75 | nc.packageExtras[name] = url 76 | result = true 77 | else: 78 | let existingPkg = nc.packageExtras[name] 79 | let existingUrl = existingPkg.url 80 | let url = url.url 81 | if existingUrl != url: 82 | if existingUrl.scheme != url.scheme and existingUrl.port == url.port and 83 | existingUrl.path == url.path and existingUrl.hostname == url.hostname: 84 | info "atlas:nimblecontext", "different url schemes for the same package:", $name, "existing:", $existingUrl, "new:", $url 85 | else: 86 | # this is handled in the solver which checks for conflicts 87 | # but users should be aware that this is happening as they can override stuff 88 | warn "atlas:nimblecontext", "name already exists in packageExtras:", $name, "isFromPath:", $isFromPath, "with different url:", $nc.packageExtras[name], "and url:", $url 89 | result = false 90 | 91 | proc put*(nc: var NimbleContext, name: string, url: PkgUrl): bool {.discardable.} = 92 | nc.putImpl(name, url, false) 93 | 94 | proc putFromPath*(nc: var NimbleContext, name: string, url: PkgUrl): bool = 95 | nc.putImpl(name, url, true) 96 | 97 | proc createUrl*(nc: var NimbleContext, nameOrig: string): PkgUrl = 98 | ## primary point to createUrl's from a name or argument 99 | doAssert not nameOrig.isAbsolute(), "createUrl does not support relative paths: " & $nameOrig 100 | 101 | var didReplace = false 102 | var name = nameOrig 103 | 104 | # First try URL overrides if it looks like a URL 105 | if nameOrig.isUrl(): 106 | name = substitute(nc.urlOverrides, nameOrig, didReplace) 107 | else: 108 | name = substitute(nc.nameOverrides, nameOrig, didReplace) 109 | 110 | if name.isUrl(): 111 | result = createUrlSkipPatterns(name) 112 | 113 | # TODO: not 100% sure this is needed, but it makes the behavior more consistent 114 | var didReplace = false 115 | name = substitute(nc.nameOverrides, result.shortName(), didReplace) 116 | if didReplace: 117 | result = createUrlSkipPatterns(name) 118 | else: 119 | let lname = nc.lookup(name) 120 | if not lname.isEmpty(): 121 | result = lname 122 | else: 123 | let lname = unicode.toLower(name) 124 | if lname notin nc.notFoundNames: 125 | warn "atlas:nimblecontext", "name not found in packages database:", $name 126 | nc.notFoundNames.incl lname 127 | raise newException(ValueError, "project name not found in packages database: " & $lname & " original: " & $nameOrig) 128 | 129 | let officialPkg = nc.lookup(result.shortName()) 130 | if not officialPkg.isEmpty() and officialPkg.url == result.url: 131 | result.hasShortName = true 132 | 133 | if not result.isEmpty(): 134 | if nc.put(result.projectName, result): 135 | debug "atlas:createUrl", "created url with name:", name, "orig:", 136 | nameOrig, "projectName:", $result.projectName, 137 | "hasShortName:", $result.hasShortName, "url:", $result.url 138 | 139 | proc createUrlFromPath*(nc: var NimbleContext, orig: Path, isLinkPath = false): PkgUrl = 140 | let absPath = absolutePath(orig) 141 | # Check if this is an Atlas project or if it's the current project 142 | let prefix = if isLinkPath: "link://" else: "atlas://" 143 | if isProject(absPath) or absPath == absolutePath(project()): 144 | if isLinkPath: 145 | let url = parseUri(prefix & $absPath) 146 | result = toPkgUriRaw(url) 147 | else: 148 | # Find nimble files in the project directory 149 | let nimbleFiles = findNimbleFile(absPath, "") 150 | if nimbleFiles.len > 0: 151 | # Use the first nimble file found as the project identifier 152 | trace "atlas:nimblecontext", "createUrlFromPath: found nimble file: ", $nimbleFiles[0] 153 | let url = parseUri(prefix & $nimbleFiles[0]) 154 | result = toPkgUriRaw(url) 155 | else: 156 | # Fallback to directory name if no nimble file found 157 | let nimble = $(absPath.splitPath().tail) & ".nimble" 158 | trace "atlas:nimblecontext", "createUrlFromPath: no nimble file found, trying directory name: ", $nimble 159 | let url = parseUri(prefix & $absPath / nimble) 160 | result = toPkgUriRaw(url) 161 | else: 162 | error "atlas:nimblecontext", "createUrlFromPath: not a project: " & $absPath 163 | # let fileUrl = "file://" & $absPath 164 | # result = createUrlSkipPatterns(fileUrl) 165 | if not result.isEmpty(): 166 | discard nc.putFromPath(result.projectName, result) 167 | 168 | proc fillPackageLookupTable(c: var NimbleContext) = 169 | let pkgsDir = packagesDirectory() 170 | if not c.hasPackageList: 171 | c.hasPackageList = true 172 | if not fileExists(pkgsDir / Path"packages.json"): 173 | updatePackages(pkgsDir) 174 | let packages = getPackageInfos(pkgsDir) 175 | var aliases: seq[PackageInfo] = @[] 176 | 177 | # add all packages to the lookup table 178 | for pkgInfo in packages: 179 | if pkgInfo.kind == pkAlias: 180 | aliases.add(pkgInfo) 181 | else: 182 | var pkgUrl = createUrlSkipPatterns(pkgInfo.url, skipDirTest=true) 183 | pkgUrl.hasShortName = true 184 | pkgUrl.qualifiedName.name = pkgInfo.name 185 | c.nameToUrl[unicode.toLower(pkgInfo.name)] = pkgUrl 186 | # c.urlToNames[pkgUrl.url] = pkgInfo.name 187 | 188 | # now we add aliases to the lookup table 189 | for pkgAlias in aliases: 190 | # first lookup the alias name 191 | let aliasName = unicode.toLower(pkgAlias.alias) 192 | let url = c.nameToUrl[aliasName] 193 | if url.isEmpty(): 194 | warn "atlas:nimblecontext", "alias name not found in nameToUrl: " & $pkgAlias, "lname:", $aliasName 195 | else: 196 | c.nameToUrl[pkgAlias.name] = url 197 | 198 | proc createUnfilledNimbleContext*(): NimbleContext = 199 | result = NimbleContext() 200 | result.nameOverrides = context().nameOverrides 201 | result.urlOverrides = context().urlOverrides 202 | # for key, val in context().packageNameOverrides: 203 | # let url = createUrlSkipPatterns($val) 204 | # result.packageExtras[key] = url 205 | # result.urlToNames[url.url()] = key 206 | 207 | proc createNimbleContext*(): NimbleContext = 208 | result = createUnfilledNimbleContext() 209 | fillPackageLookupTable(result) -------------------------------------------------------------------------------- /src/basic/nimbleparser.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Atlas Package Cloner 3 | # (c) Copyright 2023 Andreas Rumpf 4 | # 5 | # See the file "copying.txt", included in this 6 | # distribution, for details about the copyright. 7 | # 8 | 9 | import std / [os, uri, paths, strutils, json, sets] 10 | import deptypes, nimblecontext, versions, context, reporters, parse_requires, pkgurls 11 | 12 | proc addError*(err: var string; nimbleFile: string; msg: string) = 13 | if err.len > 0: err.add "\n" 14 | else: err.add "in file: " & nimbleFile & "\n" 15 | err.add msg 16 | 17 | proc processRequirement(nc: var NimbleContext; 18 | nimbleFile: Path; 19 | req: string; 20 | feature: string; 21 | result: var NimbleRelease) = 22 | let (name, reqsByFeatures, verIdx) = extractRequirementName(req) 23 | 24 | var url: PkgUrl 25 | try: 26 | url = nc.createUrl(name) # This will handle both name and URL overrides internally 27 | except ValueError, IOError, OSError: 28 | let err = getCurrentExceptionMsg() 29 | result.status = HasBrokenDep 30 | warn nimbleFile, "cannot resolve dependency package name:", name, "error:", $err 31 | result.err.addError $nimbleFile, "cannot resolve package name: " & name 32 | url = toPkgUriRaw(parseUri("error://" & name)) 33 | 34 | var err = false 35 | let query = parseVersionInterval(req, verIdx, err) # update err 36 | if err: 37 | if result.status != HasBrokenDep: 38 | warn nimbleFile, "broken nimble file: " & name 39 | result.status = HasBrokenNimbleFile 40 | result.err.addError $nimbleFile, "invalid 'requires' syntax in nimble file: " & req 41 | else: 42 | if cmpIgnoreCase(name, "nim") == 0 or cmpIgnoreCase($url, "https://github.com/nim-lang/Nim") == 0: 43 | let v = extractGeQuery(query) 44 | if v != Version"": 45 | result.nimVersion = v 46 | elif feature.len > 0: 47 | result.features.mgetOrPut(feature, @[]).add((url, query)) 48 | else: 49 | result.requirements.add((url, query)) 50 | for feature in reqsByFeatures: 51 | result.reqsByFeatures.mgetOrPut(url, initHashSet[string]()).incl(feature) 52 | 53 | proc parseNimbleFile*(nc: var NimbleContext; 54 | nimbleFile: Path): NimbleRelease = 55 | let nimbleInfo = extractRequiresInfo(nimbleFile) 56 | # let nimbleHash = secureHashFile($nimbleFile) 57 | 58 | result = NimbleRelease( 59 | hasInstallHooks: nimbleInfo.hasInstallHooks, 60 | srcDir: nimbleInfo.srcDir, 61 | status: if nimbleInfo.hasErrors: HasBrokenNimbleFile else: Normal, 62 | # nimbleHash: nimbleHash, 63 | version: parseExplicitVersion(nimbleInfo.version) 64 | ) 65 | 66 | for req in nimbleInfo.requires: 67 | processRequirement(nc, nimbleFile, req, "", result) 68 | 69 | for feature, reqs in nimbleInfo.features: 70 | for req in reqs: 71 | processRequirement(nc, nimbleFile, req, feature, result) 72 | 73 | proc genRequiresLine(u: string): string = 74 | result = "requires \"$1\"\n" % u.escape("", "") 75 | 76 | proc patchNimbleFile*(nc: var NimbleContext; 77 | nimbleFile: Path, name: string) = 78 | var url = nc.createUrl(name) # This will handle both name and URL overrides internally 79 | 80 | debug nimbleFile, "patching nimble file:", $nimbleFile, "to use package:", name, "url:", $url 81 | 82 | if url.isEmpty: 83 | error name, "cannot resolve package name: " & name 84 | return 85 | 86 | doAssert nimbleFile.string.fileExists() 87 | 88 | let release = parseNimbleFile(nc, nimbleFile) 89 | # see if we have this requirement already listed. If so, do nothing: 90 | for (dep, ver) in release.requirements: 91 | debug nimbleFile, "checking if dep url:", $url, "matches:", $dep 92 | if url == dep: 93 | info(nimbleFile, "nimble file up to date") 94 | return 95 | 96 | let name = if url.hasShortName: url.shortName() else: $url.url 97 | debug nimbleFile, "patching nimble file using:", $name 98 | 99 | let line = genRequiresLine(name) 100 | var f = open($nimbleFile, fmAppend) 101 | try: 102 | f.writeLine line 103 | finally: 104 | f.close() 105 | info(nimbleFile, "updated") 106 | -------------------------------------------------------------------------------- /src/basic/osutils.nim: -------------------------------------------------------------------------------- 1 | ## OS utilities like 'withDir'. 2 | ## (c) 2021 Andreas Rumpf 3 | 4 | import std / [os, paths, strutils, osproc, uri] 5 | import reporters 6 | 7 | export paths 8 | 9 | type 10 | PackageUrl* = Uri 11 | 12 | type 13 | ResultCode* = distinct int 14 | const 15 | RES_OK* = ResultCode(0) 16 | 17 | proc `==`*(a,b: ResultCode): bool {.borrow.} 18 | proc `$`*(a: ResultCode): string = 19 | "ResultCode($1)" % [$(int(a))] 20 | 21 | proc lastPathComponent*(s: string): string = 22 | var last = s.len - 1 23 | while last >= 0 and s[last] in {DirSep, AltSep}: dec last 24 | var first = last - 1 25 | while first >= 0 and s[first] notin {DirSep, AltSep}: dec first 26 | result = s.substr(first+1, last) 27 | 28 | proc getFilePath*(x: PackageUrl): string = 29 | assert x.scheme == "file" 30 | result = x.hostname 31 | if x.port.len() > 0: 32 | result &= ":" 33 | result &= x.port 34 | result &= x.path 35 | result &= x.query 36 | 37 | proc isUrl*(x: string): bool = 38 | x.startsWith("git://") or 39 | x.startsWith("https://") or 40 | x.startsWith("http://") or 41 | x.startsWith("file://") 42 | 43 | proc readableFile*(s: Path, path: Path): Path = 44 | if s.isRelativeTo(path): 45 | relativePath(s, path) 46 | else: 47 | s 48 | 49 | 50 | proc absoluteDepsDir*(project, value: Path): Path = 51 | if value == Path ".": 52 | result = project 53 | elif isAbsolute(value): 54 | result = value 55 | else: 56 | result = project / value 57 | 58 | 59 | proc silentExec*(cmd: string; args: openArray[string]): (string, ResultCode) = 60 | var cmdLine = cmd 61 | for i in 0.. 0: 64 | cmdLine.add quoteShell(args[i]) 65 | let (res, code) = osproc.execCmdEx(cmdLine) 66 | result = (res, ResultCode(code)) 67 | 68 | proc nimbleExec*(cmd: string; args: openArray[string]) = 69 | var cmdLine = "nimble " & cmd 70 | for i in 0.. 0: 82 | result &= " website: " & pkg.web & "\n" 83 | 84 | proc toTags*(j: JsonNode): seq[string] = 85 | result = @[] 86 | if j.kind == JArray: 87 | for elem in items j: 88 | result.add elem.getStr("") 89 | 90 | proc getPackageInfos*(pkgsDir = packagesDirectory()): seq[PackageInfo] = 91 | result = @[] 92 | var uniqueNames = initHashSet[string]() 93 | var jsonFiles = 0 94 | for kind, path in walkDir(pkgsDir): 95 | if kind == pcFile and path.string.endsWith(".json"): 96 | inc jsonFiles 97 | let packages = json.parseFile($path) 98 | for p in packages: 99 | let pkg = p.fromJson() 100 | if pkg != nil and not uniqueNames.containsOrIncl(pkg.name): 101 | result.add(pkg) 102 | 103 | proc updatePackages*(pkgsDir = packagesDirectory()) = 104 | let pkgsDir = depsDir() / DefaultPackagesSubDir 105 | if dirExists(pkgsDir): 106 | gitPull(pkgsDir) 107 | else: 108 | let res = clone(parseUri "https://github.com/nim-lang/packages", pkgsDir) 109 | if res[0] != Ok: 110 | error DefaultPackagesSubDir, "cannot clone packages repo: " & res[1] 111 | -------------------------------------------------------------------------------- /src/basic/parse_requires.nim: -------------------------------------------------------------------------------- 1 | ## Utility API for Nim package managers. 2 | ## (c) 2021 Andreas Rumpf 3 | 4 | import std / [strutils, paths, tables, options] 5 | 6 | import compiler / [ast, idents, msgs, syntaxes, options, pathutils, lineinfos] 7 | import reporters 8 | 9 | type 10 | NimbleFileInfo* = object 11 | requires*: seq[string] 12 | features*: Table[string, seq[string]] 13 | srcDir*: Path 14 | version*: string 15 | tasks*: seq[(string, string)] 16 | hasInstallHooks*: bool 17 | hasErrors*: bool 18 | 19 | proc eqIdent(a, b: string): bool {.inline.} = 20 | cmpIgnoreCase(a, b) == 0 and a[0] == b[0] 21 | 22 | proc handleError(cfg: ConfigRef, li: TLineInfo, mk: TMsgKind, msg: string) = 23 | {.cast(gcsafe).}: 24 | info("atlas:nimbleparser", "error parsing \"$1\" at $2" % [msg, cfg.toFileLineCol(li), repr mk]) 25 | 26 | proc handleError(cfg: ConfigRef, mk: TMsgKind, li: TLineInfo, msg: string) = 27 | handleError(cfg, li, warnUser, msg) 28 | 29 | proc handleError(cfg: ConfigRef, li: TLineInfo, msg: string) = 30 | handleError(cfg, warnUser, li, msg) 31 | 32 | proc compileDefines(): Table[string, bool] = 33 | result = initTable[string, bool]() 34 | result["windows"] = defined(windows) 35 | result["posix"] = defined(posix) 36 | result["linux"] = defined(linux) 37 | result["android"] = defined(android) 38 | result["macosx"] = defined(macosx) 39 | result["freebsd"] = defined(freebsd) 40 | result["openbsd"] = defined(openbsd) 41 | result["netbsd"] = defined(netbsd) 42 | result["solaris"] = defined(solaris) 43 | result["amd64"] = defined(amd64) 44 | result["x86_64"] = defined(x86_64) 45 | result["i386"] = defined(i386) 46 | result["arm"] = defined(arm) 47 | result["arm64"] = defined(arm64) 48 | result["mips"] = defined(mips) 49 | result["powerpc"] = defined(powerpc) 50 | 51 | var definedSymbols: Table[string, bool] = compileDefines() 52 | 53 | proc getBasicDefines*(): Table[string, bool] = 54 | return definedSymbols 55 | 56 | proc setBasicDefines*(sym: string, value: bool) {.inline.} = 57 | definedSymbols[sym] = value 58 | 59 | proc evalBasicDefines(sym: string; conf: ConfigRef; n: PNode): Option[bool] = 60 | if sym in definedSymbols: 61 | return some(definedSymbols[sym]) 62 | else: 63 | handleError(conf, n.info, "undefined symbol: " & sym) 64 | return none(bool) 65 | 66 | proc evalBooleanCondition(n: PNode; conf: ConfigRef): Option[bool] = 67 | ## Recursively evaluate boolean conditions in when statements 68 | case n.kind 69 | of nkCall: 70 | # Handle defined(platform) calls 71 | if n[0].kind == nkIdent and n[0].ident.s == "defined" and n.len == 2: 72 | if n[1].kind == nkIdent: 73 | return evalBasicDefines(n[1].ident.s, conf, n) 74 | return none(bool) 75 | of nkInfix: 76 | # Handle binary operators: and, or 77 | if n[0].kind == nkIdent and n.len == 3: 78 | case n[0].ident.s 79 | of "and": 80 | let left = evalBooleanCondition(n[1], conf) 81 | let right = evalBooleanCondition(n[2], conf) 82 | if left.isSome and right.isSome: 83 | return some(left.get and right.get) 84 | else: 85 | return none(bool) 86 | of "or": 87 | let left = evalBooleanCondition(n[1], conf) 88 | let right = evalBooleanCondition(n[2], conf) 89 | if left.isSome and right.isSome: 90 | return some(left.get or right.get) 91 | else: 92 | return none(bool) 93 | of "xor": 94 | let left = evalBooleanCondition(n[1], conf) 95 | let right = evalBooleanCondition(n[2], conf) 96 | if left.isSome and right.isSome: 97 | return some(left.get xor right.get) 98 | else: 99 | return none(bool) 100 | return none(bool) 101 | of nkPrefix: 102 | # Handle unary operators: not 103 | if n[0].kind == nkIdent and n[0].ident.s == "not" and n.len == 2: 104 | let inner = evalBooleanCondition(n[1], conf) 105 | if inner.isSome: 106 | return some(not inner.get) 107 | else: 108 | return none(bool) 109 | return none(bool) 110 | of nkPar: 111 | # Handle parentheses - evaluate the content 112 | if n.len == 1: 113 | return evalBooleanCondition(n[0], conf) 114 | return none(bool) 115 | of nkIdent: 116 | # Handle direct identifiers (though this shouldn't happen in practice) 117 | return evalBasicDefines(n.ident.s, conf, n) 118 | else: 119 | return none(bool) 120 | 121 | proc extract(n: PNode; conf: ConfigRef; currFeature: string; result: var NimbleFileInfo) = 122 | case n.kind 123 | of nkStmtList, nkStmtListExpr: 124 | for child in n: 125 | extract(child, conf, currFeature, result) 126 | of nkCallKinds: 127 | if n[0].kind == nkIdent: 128 | case n[0].ident.s 129 | of "requires": 130 | for i in 1.. 0: ch = ch.lastSon 133 | if ch.kind in {nkStrLit..nkTripleStrLit}: 134 | if currFeature.len > 0: 135 | result.features[currFeature].add ch.strVal 136 | else: 137 | result.requires.add ch.strVal 138 | else: 139 | handleError(conf, ch.info, "'requires' takes string literals") 140 | # result.hasErrors = true 141 | of "task": 142 | if n.len >= 3 and n[1].kind == nkIdent and n[2].kind in {nkStrLit..nkTripleStrLit}: 143 | result.tasks.add((n[1].ident.s, n[2].strVal)) 144 | of "before", "after": 145 | #[ 146 | before install do: 147 | exec "git submodule update --init" 148 | var make = "make" 149 | when defined(windows): 150 | make = "mingw32-make" 151 | exec make 152 | ]# 153 | if n.len >= 3 and n[1].kind == nkIdent and n[1].ident.s == "install": 154 | result.hasInstallHooks = true 155 | of "feature": 156 | if n.len >= 3: 157 | var features = newSeq[string]() 158 | for i in 1 ..< n.len - 1: 159 | let c = n[i] 160 | if c.kind == nkStrLit: 161 | features.add(c.strVal) 162 | else: 163 | handleError(conf, n.info, "feature requires string literals") 164 | # result.hasErrors = true 165 | for f in features: 166 | result.features[f] = newSeq[string]() 167 | extract(n[^1], conf, f, result) 168 | else: 169 | discard 170 | of nkAsgn, nkFastAsgn: 171 | if n[0].kind == nkIdent and eqIdent(n[0].ident.s, "srcDir"): 172 | if n[1].kind in {nkStrLit..nkTripleStrLit}: 173 | result.srcDir = Path n[1].strVal 174 | else: 175 | handleError(conf, n[1].info, "assignments to 'srcDir' must be string literals") 176 | # result.hasErrors = true 177 | elif n[0].kind == nkIdent and eqIdent(n[0].ident.s, "version"): 178 | if n[1].kind in {nkStrLit..nkTripleStrLit}: 179 | result.version = n[1].strVal 180 | else: 181 | handleError(conf, n[1].info, "assignments to 'version' must be string literals") 182 | # result.hasErrors = true 183 | of nkWhenStmt: 184 | # handles arbitrary boolean conditions in when statements 185 | if n[0].kind == nkElifBranch: 186 | let cond = n[0][0] 187 | let body = n[0][1] 188 | 189 | # Use the new recursive boolean evaluator 190 | let condResult = evalBooleanCondition(cond, conf) 191 | if condResult.isSome: 192 | if condResult.get: 193 | extract(body, conf, currFeature, result) 194 | else: 195 | handleError(conf, n.info, "when statement condition is not a boolean or uses undefined symbols") 196 | # result.hasErrors = true 197 | else: 198 | discard 199 | 200 | proc extractRequiresInfo*(nimbleFile: Path): NimbleFileInfo = 201 | ## Extract the `requires` information from a Nimble file. This does **not** 202 | ## evaluate the Nimble file. Errors are produced on stderr/stdout and are 203 | ## formatted as the Nim compiler does it. The parser uses the Nim compiler 204 | ## as an API. The result can be empty, this is not an error, only parsing 205 | ## errors are reported. 206 | var conf = newConfigRef() 207 | conf.foreignPackageNotes = {} 208 | conf.notes = {} 209 | conf.mainPackageNotes = {} 210 | conf.errorMax = high(int) 211 | conf.structuredErrorHook = proc (config: ConfigRef; info: TLineInfo; msg: string; 212 | severity: Severity) {.gcsafe.} = 213 | handleError(config, info, warnUser, msg) 214 | 215 | let fileIdx = fileInfoIdx(conf, AbsoluteFile nimbleFile) 216 | var parser: Parser 217 | parser.lex.errorHandler = proc (config: ConfigRef, info: TLineInfo, mk: TMsgKind, msg: string;) {.closure, gcsafe.} = 218 | handleError(config, info, mk, msg) 219 | 220 | if setupParser(parser, fileIdx, newIdentCache(), conf): 221 | extract(parseAll(parser), conf, "", result) 222 | closeParser(parser) 223 | result.hasErrors = result.hasErrors or conf.errorCounter > 0 224 | 225 | type 226 | PluginInfo* = object 227 | builderPatterns*: seq[(string, string)] 228 | 229 | proc extractPlugin(nimscriptFile: string; n: PNode; conf: ConfigRef; result: var PluginInfo) = 230 | case n.kind 231 | of nkStmtList, nkStmtListExpr: 232 | for child in n: 233 | extractPlugin(nimscriptFile, child, conf, result) 234 | of nkCallKinds: 235 | if n[0].kind == nkIdent: 236 | case n[0].ident.s 237 | of "builder": 238 | if n.len >= 3 and n[1].kind in {nkStrLit..nkTripleStrLit}: 239 | result.builderPatterns.add((n[1].strVal, nimscriptFile)) 240 | else: discard 241 | else: 242 | discard 243 | 244 | proc extractPluginInfo*(nimscriptFile: string; info: var PluginInfo) = 245 | var conf = newConfigRef() 246 | conf.foreignPackageNotes = {} 247 | conf.notes = {} 248 | conf.mainPackageNotes = {} 249 | 250 | let fileIdx = fileInfoIdx(conf, AbsoluteFile nimscriptFile) 251 | var parser: Parser 252 | if setupParser(parser, fileIdx, newIdentCache(), conf): 253 | extractPlugin(nimscriptFile, parseAll(parser), conf, info) 254 | closeParser(parser) 255 | 256 | const Operators* = {'<', '>', '=', '&', '@', '!', '^'} 257 | 258 | proc token(s: string; idx: int; lit: var string): int = 259 | var i = idx 260 | if i >= s.len: return i 261 | while s[i] in Whitespace: inc(i) 262 | case s[i] 263 | of Letters, '#': 264 | lit.add s[i] 265 | inc i 266 | while i < s.len and s[i] notin (Whitespace + {'@', '#'}): 267 | lit.add s[i] 268 | inc i 269 | of '0'..'9': 270 | while i < s.len and s[i] in {'0'..'9', '.'}: 271 | lit.add s[i] 272 | inc i 273 | of '"': 274 | inc i 275 | while i < s.len and s[i] != '"': 276 | lit.add s[i] 277 | inc i 278 | inc i 279 | of Operators: 280 | while i < s.len and s[i] in Operators: 281 | lit.add s[i] 282 | inc i 283 | else: 284 | lit.add s[i] 285 | inc i 286 | result = i 287 | 288 | iterator tokenizeRequires*(s: string): string = 289 | var start = 0 290 | var tok = "" 291 | while start < s.len: 292 | tok.setLen 0 293 | start = token(s, start, tok) 294 | yield tok 295 | 296 | when isMainModule: 297 | for x in tokenizeRequires("jester@#head >= 1.5 & <= 1.8"): 298 | echo x 299 | 300 | let badInfo = extractRequiresInfo(Path"tests/test_data/bad.nimble") 301 | echo "bad nimble info: ", repr(badInfo) 302 | 303 | echo "\n--- Testing boolean logic parsing ---" 304 | let jesterInfo = extractRequiresInfo(Path"tests/test_data/jester_boolean.nimble") 305 | echo "jester boolean nimble info: ", repr(jesterInfo) 306 | -------------------------------------------------------------------------------- /src/basic/pkgurls.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Atlas Package Cloner 3 | # (c) Copyright 2024 Andreas Rumpf 4 | # 5 | # See the file "copying.txt", included in this 6 | # distribution, for details about the copyright. 7 | # 8 | 9 | import std / [hashes, uri, os, strutils, files, dirs, sequtils, pegs, json, jsonutils] 10 | import gitops, reporters, context 11 | 12 | export uri 13 | 14 | const 15 | GitSuffix = ".git" 16 | 17 | type 18 | PkgUrl* = object 19 | qualifiedName*: tuple[name: string, user: string, host: string] 20 | hasShortName*: bool 21 | u: Uri 22 | 23 | proc isFileProtocol*(s: PkgUrl): bool = s.u.scheme == "file" 24 | proc isEmpty*(s: PkgUrl): bool = s.qualifiedName[0].len() == 0 or $s.u == "" 25 | proc isUrl*(s: string): bool = s.startsWith("git@") or "://" in s 26 | 27 | proc fullName*(u: PkgUrl): string = 28 | if u.qualifiedName.host.len() > 0 or u.qualifiedName.user.len() > 0: 29 | result = u.qualifiedName.name & "." & u.qualifiedName.user & "." & u.qualifiedName.host 30 | else: 31 | result = u.qualifiedName.name 32 | 33 | proc shortName*(u: PkgUrl): string = 34 | u.qualifiedName.name 35 | 36 | proc projectName*(u: PkgUrl): string = 37 | if u.hasShortName or u.qualifiedName.host == "": 38 | u.qualifiedName.name 39 | else: 40 | u.qualifiedName.name & "." & u.qualifiedName.user & "." & u.qualifiedName.host 41 | 42 | proc requiresName*(u: PkgUrl): string = 43 | if u.hasShortName: 44 | u.qualifiedName.name 45 | else: 46 | $u.u 47 | 48 | proc toUri*(u: PkgUrl): Uri = result = u.u 49 | proc url*(p: PkgUrl): Uri = p.u 50 | proc `$`*(u: PkgUrl): string = $u.u 51 | proc hash*(a: PkgUrl): Hash {.inline.} = hash(a.u) 52 | proc `==`*(a, b: PkgUrl): bool {.inline.} = a.u == b.u 53 | 54 | proc toReporterName(u: PkgUrl): string = u.projectName() 55 | 56 | proc extractProjectName*(url: Uri): tuple[name: string, user: string, host: string] = 57 | var u = url 58 | var (p, n, e) = u.path.splitFile() 59 | p.removePrefix(DirSep) 60 | p.removePrefix(AltSep) 61 | if u.scheme in ["http", "https"] and e == GitSuffix: 62 | e = "" 63 | 64 | if u.scheme == "atlas": 65 | result = (n, "", "") 66 | elif u.scheme == "file": 67 | result = (n & e, "", "") 68 | elif u.scheme == "link": 69 | result = (n, "", "") 70 | else: 71 | result = (n & e, p, u.hostname) 72 | 73 | proc toOriginalPath*(pkgUrl: PkgUrl, isWindowsTest: bool = false): Path = 74 | if pkgUrl.url.scheme in ["file", "link", "atlas"]: 75 | result = Path(pkgUrl.url.hostname & pkgUrl.url.path) 76 | if defined(windows) or isWindowsTest: 77 | var p = result.string.replace('/', '\\') 78 | p.removePrefix('\\') 79 | result = p.Path 80 | else: 81 | raise newException(ValueError, "Invalid file path: " & $pkgUrl.url) 82 | 83 | proc linkPath*(path: Path): Path = 84 | result = Path(path.string & ".nimble-link") 85 | 86 | proc toDirectoryPath(pkgUrl: PkgUrl, isLinkFile: bool): Path = 87 | trace pkgUrl, "directory path from:", $pkgUrl.url 88 | 89 | if pkgUrl.url.scheme == "atlas": 90 | result = project() 91 | elif pkgUrl.url.scheme == "link": 92 | result = pkgUrl.toOriginalPath().parentDir() 93 | elif pkgUrl.url.scheme == "file": 94 | # file:// urls are used for local source paths, not dependency paths 95 | result = depsDir() / Path(pkgUrl.projectName()) 96 | else: 97 | result = depsDir() / Path(pkgUrl.projectName()) 98 | 99 | if not isLinkFile and not dirExists(result) and fileExists(result.linkPath()): 100 | # prefer the directory path if it exists (?) 101 | let linkPath = result.linkPath() 102 | let link = readFile($linkPath) 103 | let lines = link.split("\n") 104 | if lines.len != 2: 105 | warn pkgUrl.projectName(), "invalid link file:", $linkPath 106 | else: 107 | let nimble = Path(lines[0]) 108 | result = nimble.splitFile().dir 109 | if not result.isAbsolute(): 110 | result = linkPath.parentDir() / result 111 | debug pkgUrl.projectName(), "link file to:", $result 112 | 113 | result = result.absolutePath 114 | trace pkgUrl, "found directory path:", $result 115 | doAssert result.len() > 0 116 | 117 | proc toDirectoryPath*(pkgUrl: PkgUrl): Path = 118 | toDirectoryPath(pkgUrl, false) 119 | 120 | proc toLinkPath*(pkgUrl: PkgUrl): Path = 121 | if pkgUrl.url.scheme == "atlas": 122 | result = Path("") 123 | elif pkgUrl.url.scheme == "link": 124 | result = depsDir() / Path(pkgUrl.projectName() & ".nimble-link") 125 | else: 126 | result = Path(toDirectoryPath(pkgUrl, true).string & ".nimble-link") 127 | 128 | proc isLinkPath*(pkgUrl: PkgUrl): bool = 129 | result = fileExists(toLinkPath(pkgUrl)) 130 | 131 | proc isAtlasProject*(pkgUrl: PkgUrl): bool = 132 | result = pkgUrl.url.scheme == "link" 133 | 134 | proc createNimbleLink*(pkgUrl: PkgUrl, nimblePath: Path, cfgPath: CfgPath) = 135 | let nimbleLink = toLinkPath(pkgUrl) 136 | trace "nimble:link", "creating link at:", $nimbleLink, "from:", $nimblePath 137 | if nimbleLink.fileExists(): 138 | return 139 | 140 | let nimblePath = nimblePath.absolutePath() 141 | let cfgPath = cfgPath.Path.absolutePath() 142 | 143 | writeFile($nimbleLink, "$1\n$2" % [$nimblePath, $cfgPath]) 144 | 145 | proc isWindowsAbsoluteFile*(raw: string): bool = 146 | raw.match(peg"^ {'file://'?} {[A-Z] ':' ['/'\\]} .*") or 147 | raw.match(peg"^ {'link://'?} {[A-Z] ':' ['/'\\]} .*") or 148 | raw.match(peg"^ {'atlas://'?} {[A-Z] ':' ['/'\\]} .*") 149 | 150 | proc toWindowsFileUrl*(raw: string): string = 151 | let rawPath = raw.replace('\\', '/') 152 | if rawPath.isWindowsAbsoluteFile(): 153 | result = rawPath 154 | result = result.replace("file://", "file:///") 155 | result = result.replace("link://", "link:///") 156 | result = result.replace("atlas://", "atlas:///") 157 | else: 158 | result = rawPath 159 | 160 | proc fixFileRelativeUrl*(u: Uri, isWindowsTest: bool = false): Uri = 161 | if isWindowsTest or defined(windows) and u.scheme in ["file", "link", "atlas"] and u.hostname.len() > 0: 162 | result = parseUri(toWindowsFileUrl($u)) 163 | else: 164 | result = u 165 | 166 | if result.scheme in ["file", "link", "atlas"] and result.hostname.len() > 0: 167 | # fix relative paths 168 | var url = (project().string / (result.hostname & result.path)).absolutePath 169 | url = result.scheme & "://" & url 170 | if isWindowsTest or defined(windows): 171 | url = toWindowsFileUrl(url) 172 | result = parseUri(url) 173 | 174 | proc createUrlSkipPatterns*(raw: string, skipDirTest = false, forceWindows: bool = false): PkgUrl = 175 | template cleanupUrl(u: Uri) = 176 | if u.path.endsWith(".git") and (u.scheme in ["http", "https"] or u.hostname in ["github.com", "gitlab.com", "bitbucket.org"]): 177 | u.path.removeSuffix(".git") 178 | 179 | u.path = u.path.strip(leading=false, trailing=true, {'/'}) 180 | 181 | if not raw.isUrl(): 182 | if dirExists(raw) or skipDirTest: 183 | var raw: string = raw 184 | if isGitDir(raw): 185 | raw = getRemoteUrl(Path(raw)) 186 | else: 187 | if not forceWindows: 188 | raw = raw.absolutePath() 189 | if forceWindows or defined(windows) or defined(atlasUnitTests): 190 | raw = toWindowsFileUrl("file:///" & raw) 191 | else: 192 | raw = "file://" & raw 193 | let u = parseUri(raw) 194 | result = PkgUrl(qualifiedName: extractProjectName(u), u: u, hasShortName: true) 195 | else: 196 | raise newException(ValueError, "Invalid name or URL: " & raw) 197 | elif raw.startsWith("git@"): # special case git@server.com 198 | var u = parseUri("ssh://" & raw.replace(":", "/")) 199 | cleanupUrl(u) 200 | result = PkgUrl(qualifiedName: extractProjectName(u), u: u, hasShortName: false) 201 | else: 202 | var u = parseUri(raw) 203 | var hasShortName = false 204 | 205 | if u.scheme == "git": 206 | if u.port.anyIt(not it.isDigit()): 207 | u.path = "/" & u.port & u.path 208 | u.port = "" 209 | 210 | u.scheme = "ssh" 211 | 212 | if u.scheme in ["file", "link", "atlas"]: 213 | # fix missing absolute paths 214 | u = fixFileRelativeUrl(u, isWindowsTest = forceWindows) 215 | hasShortName = true 216 | 217 | cleanupUrl(u) 218 | result = PkgUrl(qualifiedName: extractProjectName(u), u: u, hasShortName: hasShortName) 219 | # trace result, "created url raw:", repr(raw), "url:", repr(result) 220 | 221 | proc toPkgUriRaw*(u: Uri, hasShortName: bool = false): PkgUrl = 222 | result = createUrlSkipPatterns($u, true) 223 | result.hasShortName = hasShortName 224 | -------------------------------------------------------------------------------- /src/basic/reporters.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Atlas Package Cloner 3 | # (c) Copyright 2023 Andreas Rumpf 4 | # 5 | # See the file "copying.txt", included in this 6 | # distribution, for details about the copyright. 7 | # 8 | 9 | import std / [terminal, paths] 10 | export paths 11 | 12 | type 13 | MsgKind* = enum 14 | Ignore = "" 15 | Error = "[Error] " 16 | Warning = "[Warn] ", 17 | Notice = "[Notice] ", 18 | Info = "[Info] ", 19 | Debug = "[Debug] " 20 | Trace = "[Trace] " 21 | 22 | Reporter* = object of RootObj 23 | verbosity*: MsgKind 24 | noColors*: bool 25 | assertOnError*: bool 26 | errorsColor* = fgRed 27 | warnings*: int 28 | errors*: int 29 | messages: seq[(MsgKind, string, seq[string])] # delayed output 30 | 31 | var atlasReporter* = Reporter(verbosity: Notice) 32 | 33 | proc setAtlasVerbosity*(verbosity: MsgKind) = 34 | atlasReporter.verbosity = verbosity 35 | 36 | proc setAtlasNoColors*(nc: bool) = 37 | atlasReporter.noColors = nc 38 | 39 | proc setAtlasAssertOnError*(err: bool) = 40 | atlasReporter.assertOnError = err 41 | 42 | proc atlasErrors*(): int = 43 | atlasReporter.errors 44 | 45 | proc setAtlasErrorsColor*(color: ForegroundColor) = 46 | atlasReporter.errorsColor = color 47 | 48 | proc writeMessageRaw(c: var Reporter; category: string; p: string, args: seq[string]) = 49 | var msg = category 50 | if p.len > 0: msg.add "(" & p & ") " 51 | for arg in args: 52 | msg.add arg 53 | msg.add " " 54 | stdout.writeLine msg 55 | 56 | proc writeMessage(c: var Reporter; k: MsgKind; p: string, args: seq[string]) = 57 | if k == Ignore: return 58 | if k > c.verbosity: return 59 | # if k == Trace and c.verbosity < 1: return 60 | # elif k == Debug and c.verbosity < 2: return 61 | 62 | if c.noColors: 63 | writeMessageRaw(c, $k, p, args) 64 | else: 65 | let (color, style) = 66 | case k 67 | of Ignore: (fgWhite, styleDim) 68 | of Trace: (fgWhite, styleDim) 69 | of Debug: (fgBlue, styleBright) 70 | of Info: (fgGreen, styleBright) 71 | of Notice: (fgMagenta, styleBright) 72 | of Warning: (fgYellow, styleBright) 73 | of Error: (c.errorsColor, styleBright) 74 | 75 | stdout.styledWrite(color, style, $k, resetStyle, fgCyan, "(", p, ")", resetStyle) 76 | let colors = [fgWhite, fgMagenta] 77 | for idx, arg in args: 78 | stdout.styledWrite(colors[idx mod 2], " ", arg) 79 | stdout.styledWriteLine(resetStyle, "") 80 | 81 | proc message(c: var Reporter; k: MsgKind; p: string, args: openArray[string]) = 82 | ## collects messages or prints them out immediately 83 | # c.messages.add (k, p, arg) 84 | if c.assertOnError: 85 | raise newException(AssertionDefect, p & ": " & $args) 86 | if k == Warning: 87 | inc c.warnings 88 | elif k == Error: 89 | inc c.errors 90 | writeMessage c, k, p, @args 91 | 92 | proc writePendingMessages*(c: var Reporter) = 93 | for i in 0.. 0: 62 | ctx.depsDir = m.deps.Path 63 | 64 | # Handle package name overrides 65 | for key, val in m.nameOverrides: 66 | let err = ctx.nameOverrides.addPattern(key, val) 67 | if err.len > 0: 68 | error configFile, "invalid name override pattern: " & err 69 | 70 | # Handle URL overrides 71 | for key, val in m.urlOverrides: 72 | let err = ctx.urlOverrides.addPattern(key, val) 73 | if err.len > 0: 74 | error configFile, "invalid URL override pattern: " & err 75 | 76 | # Handle package overrides 77 | for key, val in m.pkgOverrides: 78 | ctx.pkgOverrides[key] = parseUri(val) 79 | if m.resolver.len > 0: 80 | try: 81 | ctx.defaultAlgo = parseEnum[ResolutionAlgorithm](m.resolver) 82 | except ValueError: 83 | warn configFile, "ignored unknown resolver: " & m.resolver 84 | if m.plugins.len > 0: 85 | ctx.pluginsFile = m.plugins.Path 86 | readPluginsDir(m.plugins.Path) 87 | 88 | 89 | proc readConfig*() = 90 | readAtlasContext(context(), project()) 91 | # trace "atlas:config", "read config file: ", repr context() 92 | 93 | proc writeConfig*() = 94 | # TODO: serialize graph in a smarter way 95 | 96 | let config = JsonConfig( 97 | deps: $depsDir(relative=true), 98 | nameOverrides: context().nameOverrides.toTable(), 99 | urlOverrides: context().urlOverrides.toTable(), 100 | pkgOverrides: context().pkgOverrides.pairs().toSeq().mapIt((it[0], $it[1])).toTable(), 101 | plugins: $context().pluginsFile, 102 | resolver: $context().defaultAlgo, 103 | graph: newJNull() 104 | ) 105 | 106 | let jcfg = toJson(config) 107 | doAssert not jcfg.isNil() 108 | let configFile = getProjectConfig() 109 | debug "atlas", "writing config file: ", $configFile 110 | writeFile($configFile, pretty(jcfg)) 111 | 112 | proc writeDepGraph*(g: DepGraph, debug: bool = false) = 113 | var configFile = depGraphCacheFile(context()) 114 | if debug: 115 | configFile = configFile.changeFileExt("debug.json") 116 | debug "atlas", "writing dep graph to: ", $configFile 117 | dumpJson(g, $configFile, pretty = true) 118 | 119 | proc readDepGraph*(nc: var NimbleContext, ctx: AtlasContext, path: Path): DepGraph = 120 | let configFile = depGraphCacheFile(ctx) 121 | debug "atlas", "reading dep graph from: ", $configFile 122 | result = loadJson(nc, $configFile) 123 | 124 | proc loadDepGraph*(nc: var NimbleContext, nimbleFile: Path): DepGraph = 125 | doAssert nimbleFile.isAbsolute() and endsWith($nimbleFile, ".nimble") and fileExists($nimbleFile) 126 | let projectDir = nimbleFile.parentDir() 127 | var ctx = AtlasContext(projectDir: projectDir) 128 | readAtlasContext(ctx, projectDir) 129 | let configFile = depGraphCacheFile(ctx) 130 | debug "atlas", "reading dep graph from: ", $configFile 131 | result = loadJson(nc, $configFile) 132 | -------------------------------------------------------------------------------- /src/lockfiles.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Atlas Package Cloner 3 | # (c) Copyright 2023 Andreas Rumpf 4 | # 5 | # See the file "copying.txt", included in this 6 | # distribution, for details about the copyright. 7 | # 8 | 9 | ## Lockfile implementation. 10 | 11 | import std / [sequtils, paths, dirs, files, strutils, tables, sets, os, json, jsonutils] 12 | import basic/[lockfiletypes, context, pkgurls, osutils, gitops, nimblechecksums, compilerversions, 13 | configutils, depgraphtypes, reporters, nimblecontext] 14 | import depgraphs, dependencies 15 | 16 | const 17 | NimbleLockFileName* = Path "nimble.lock" 18 | 19 | 20 | proc prefixedPath*(path: Path): Path = 21 | let parts = splitPath($path) 22 | if path.isRelativeTo(depsDir()): 23 | return Path("$deps" / parts.tail) 24 | elif path.isRelativeTo(project()): 25 | return Path("$project" / parts.tail) 26 | else: 27 | return Path($path) 28 | 29 | proc fromPrefixedPath*(path: Path): Path = 30 | var path = path 31 | if path.string.startsWith("$deps"): 32 | path.string.removePrefix("$deps") 33 | return depsDir() / path 34 | elif path.string.startsWith("$project"): 35 | path.string.removePrefix("$project") # default to deps dir now 36 | return depsDir() / path 37 | else: 38 | return path 39 | 40 | proc genLockEntry(lf: var LockFile; w: Package) = 41 | lf.items[w.url.projectName()] = LockFileEntry( 42 | dir: prefixedPath(w.ondisk), 43 | url: $w.url.url, 44 | commit: $currentGitCommit(w.ondisk), 45 | version: if w.activeVersion.isNil: "" else: $w.activeVersion.vtag.v 46 | ) 47 | 48 | proc newLockFile(): LockFile = 49 | result = LockFile(items: initOrderedTable[string, LockFileEntry](), 50 | hostOS: system.hostOS, hostCPU: system.hostCPU, 51 | nimVersion: detectNimVersion(), 52 | gccVersion: detectGccVersion(), 53 | clangVersion: detectClangVersion()) 54 | 55 | type 56 | NimbleLockFileEntry* = object 57 | version*: string 58 | vcsRevision*: string 59 | url*: string 60 | downloadMethod*: string 61 | dependencies*: seq[string] 62 | checksums*: Table[string, string] 63 | 64 | NimbleLockFile* = object # serialized as JSON 65 | packages*: OrderedTable[string, NimbleLockFileEntry] 66 | version*: int 67 | 68 | proc newNimbleLockFile(): NimbleLockFile = 69 | let tbl = initOrderedTable[string, NimbleLockFileEntry]() 70 | result = NimbleLockFile(version: 1, 71 | packages: tbl) 72 | 73 | proc write(lock: NimbleLockFile; lockFilePath: string) = 74 | writeFile lockFilePath, pretty(toJson(lock)) 75 | 76 | proc genLockEntry(lf: var NimbleLockFile; 77 | w: Package, 78 | cfg: CfgPath, 79 | deps: HashSet[string]) = 80 | let nimbleFiles = findNimbleFile(w) 81 | let nimbleFile = 82 | if nimbleFiles.len() == 1: 83 | nimbleFiles[0] 84 | else: 85 | error w.url.projectName, "Couldn't find nimble file at " & $w.ondisk 86 | return 87 | 88 | let info = extractRequiresInfo(nimbleFile) 89 | let commit = currentGitCommit(w.ondisk) 90 | infoNow w.url.projectName, "calculating nimble checksum" 91 | let chk = nimbleChecksum(w.url.projectName, w.ondisk) 92 | lf.packages[w.url.projectName] = NimbleLockFileEntry( 93 | version: info.version, 94 | vcsRevision: $commit, 95 | url: $w.url.url, 96 | downloadMethod: "git", 97 | dependencies: deps.mapIt(it), 98 | checksums: {"sha1": chk}.toTable 99 | ) 100 | 101 | const 102 | NimCfg = Path "nim.cfg" 103 | 104 | proc pinGraph*(graph: DepGraph; lockFile: Path; exportNimble = false) = 105 | info "pin", "pinning project" 106 | var lf = newLockFile() 107 | let project = project() 108 | 109 | # only used for exporting nimble locks 110 | var nlf = newNimbleLockFile() 111 | var nimbleDeps = newTable[string, HashSet[string]]() 112 | 113 | info project, "pinning lockfile: " & $lockFile 114 | 115 | var nc = createNimbleContext() 116 | var graph = project.expandGraph(nc, CurrentCommit, onClone=DoNothing) 117 | 118 | for pkg in toposorted(graph): 119 | if pkg.isRoot: 120 | continue 121 | 122 | let dir = pkg.ondisk 123 | if not exportNimble: 124 | # generate atlas native lockfile entries 125 | genLockEntry lf, pkg 126 | else: 127 | # handle exports for Nimble; these require looking up a bit more info 128 | for nx in directDependencies(graph, pkg): 129 | nimbleDeps.mgetOrPut(pkg.url.projectName, 130 | initHashSet[string]()).incl(nx.url.projectName) 131 | debug pkg.url.projectName, "exporting nimble " & $pkg.url.url 132 | let deps = nimbleDeps.getOrDefault(pkg.url.projectName) 133 | genLockEntry nlf, pkg, getCfgPath(graph, pkg), deps 134 | 135 | let nimcfgPath = project / NimCfg 136 | if fileExists(nimcfgPath): 137 | lf.nimcfg = readFile($nimcfgPath).splitLines() 138 | 139 | let nimblePaths = findNimbleFile(project) 140 | if nimblePaths.len() == 1 and nimblePaths[0].string.len > 0 and nimblePaths[0].fileExists(): 141 | lf.nimbleFile = LockedNimbleFile( 142 | filename: nimblePaths[0].relativePath(project), 143 | content: readFile($nimblePaths[0]).splitLines()) 144 | 145 | if not exportNimble: 146 | write lf, $lockFile 147 | else: 148 | write nlf, $lockFile 149 | 150 | proc pinProject*(lockFile: Path, exportNimble = false) = 151 | ## Pin project using deps starting from the current project directory. 152 | ## 153 | notice "atlas:pin", "Pinning project:", $lockFile 154 | let project = project() 155 | 156 | var nc = createNimbleContext() 157 | let graph = project.expandGraph(nc, CurrentCommit, onClone=DoNothing) 158 | pinGraph graph, lockFile 159 | 160 | proc compareVersion(key, wanted, got: string) = 161 | if wanted != got: 162 | warn key, "environment mismatch: " & 163 | " versions differ: previously used: " & wanted & " but now at: " & got 164 | 165 | proc convertNimbleLock*(nimble: Path): LockFile = 166 | ## converts nimble lock file into a Atlas lockfile 167 | ## 168 | let jsonAsStr = readFile($nimble) 169 | let jsonTree = parseJson(jsonAsStr) 170 | 171 | if jsonTree.getOrDefault("version") == nil or 172 | "packages" notin jsonTree: 173 | error nimble, "invalid nimble lockfile" 174 | return 175 | 176 | var nc = createNimbleContext() 177 | 178 | result = newLockFile() 179 | for (name, info) in jsonTree["packages"].pairs: 180 | if name == "nim": 181 | result.nimVersion = info["version"].getStr 182 | else: 183 | # lookup package using url 184 | let pkgurl = info["url"].getStr 185 | info name, " imported " 186 | let u = nc.createUrl(pkgurl) 187 | let dir = depsDir(relative=true) / u.projectName.Path 188 | result.items[name] = LockFileEntry( 189 | dir: dir.relativePath(project()), 190 | url: pkgurl, 191 | commit: info["vcsRevision"].getStr 192 | ) 193 | 194 | proc listChanged*(lockFile: Path) = 195 | ## list any packages that differ from the lockfile 196 | ## 197 | let lf = if lockFile == NimbleLockFileName: 198 | convertNimbleLock(lockFile) 199 | else: 200 | readLockFile(lockFile) 201 | 202 | let base = splitPath(lockFile).head 203 | 204 | # update the the dependencies 205 | for _, v in pairs(lf.items): 206 | let dir = fromPrefixedPath(v.dir) 207 | if not dirExists(dir): 208 | warn dir, "repo missing!" 209 | continue 210 | withDir dir: 211 | let url = $getRemoteUrl(dir) 212 | if v.url != url: 213 | warn v.dir, "remote URL has been changed;" & 214 | " found: " & url & 215 | " lockfile has: " & v.url 216 | 217 | let commit = currentGitCommit(dir) 218 | if commit.h != v.commit: 219 | warn dir, "commit differs;" & 220 | " found: " & $commit & 221 | " lockfile has: " & v.commit 222 | else: 223 | notice "atlas:pin", "Repo:", $dir.relativePath(project()), "is up to date at:", commit.short() 224 | 225 | if lf.hostOS == system.hostOS and lf.hostCPU == system.hostCPU: 226 | compareVersion "nim", lf.nimVersion, detectNimVersion() 227 | compareVersion "gcc", lf.gccVersion, detectGccVersion() 228 | compareVersion "clang", lf.clangVersion, detectClangVersion() 229 | 230 | proc replay*(lockFile: Path) = 231 | ## replays the given lockfile by cloning and updating all the deps 232 | ## 233 | ## this also includes updating the nim.cfg and nimble file as well 234 | ## if they're included in the lockfile 235 | ## 236 | let project = project() 237 | let lf = if lockFile == NimbleLockFileName: 238 | convertNimbleLock(lockFile) 239 | else: 240 | readLockFile(lockFile) 241 | 242 | var genCfg = CfgHere in context().flags 243 | var nc = createNimbleContext() 244 | 245 | # update the nim.cfg file 246 | if lf.nimcfg.len > 0: 247 | writeFile($(project / NimCfg), lf.nimcfg.join("\n")) 248 | else: 249 | genCfg = true 250 | 251 | # update the nimble file 252 | if lf.nimbleFile.filename.string.len > 0: 253 | writeFile($(project / lf.nimbleFile.filename), 254 | lf.nimbleFile.content.join("\n")) 255 | 256 | # update the the dependencies 257 | var paths: seq[CfgPath] = @[] 258 | for _, v in pairs(lf.items): 259 | let dir = fromPrefixedPath(v.dir) 260 | notice "atlas:replay", "Setting up repo:", $dir.relativePath(project()), "to commit:", $v.commit 261 | if not dirExists(dir): 262 | let (status, err) = gitops.clone(nc.createUrl(v.url).toUri, dir) 263 | if status != Ok: 264 | error lockFile, err 265 | continue 266 | 267 | let url = $getRemoteUrl(dir) 268 | if $url.createUrlSkipPatterns() != url: 269 | let lvl = if IgnoreGitRemoteUrls in context().flags: Info else: Error 270 | message lvl, v.dir, "remote URL differs from expected: got: " & 271 | url & " but expected: " & v.url 272 | 273 | let commit = v.commit.initCommitHash(FromLockfile) 274 | if not checkoutGitCommitFull(dir, commit): 275 | error v.dir, "unable to convert to full clone:", $v.commit, "at:", $dir 276 | 277 | if genCfg: 278 | paths.add findCfgDir(dir) 279 | 280 | if genCfg: 281 | # this allows us to re-create a nim.cfg that uses the paths from the users project 282 | # without needing to do a `installDependencies` or `traverseLoop` 283 | let cfgPath = CfgPath(project) 284 | patchNimCfg(paths, cfgPath) 285 | 286 | if lf.hostOS == system.hostOS and lf.hostCPU == system.hostCPU: 287 | compareVersion "nim", lf.nimVersion, detectNimVersion() 288 | compareVersion "gcc", lf.gccVersion, detectGccVersion() 289 | compareVersion "clang", lf.clangVersion, detectClangVersion() 290 | -------------------------------------------------------------------------------- /src/nimenv.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Atlas Package Cloner 3 | # (c) Copyright 2023 Andreas Rumpf 4 | # 5 | # See the file "copying.txt", included in this 6 | # distribution, for details about the copyright. 7 | # 8 | 9 | ## Implementation of the "Nim virtual environment" (`atlas env`) feature. 10 | import std/[files, dirs, strscans, os, strutils] 11 | import basic/[context, osutils, versions, gitops] 12 | 13 | when defined(windows): 14 | const 15 | BatchFile = """ 16 | @echo off 17 | set PATH="$1";%PATH% 18 | """ 19 | else: 20 | const 21 | ShellFile* = "export PATH=$1:$$PATH\n" 22 | 23 | const 24 | ActivationFile* = when defined(windows): Path "activate.bat" else: Path "activate.sh" 25 | 26 | template withDir*(dir: string; body: untyped) = 27 | let old = paths.getCurrentDir() 28 | try: 29 | setCurrentDir(dir) 30 | # echo "WITHDIR: ", dir, " at: ", getCurrentDir() 31 | body 32 | finally: 33 | setCurrentDir(old) 34 | 35 | proc infoAboutActivation(nimDest: Path, nimVersion: string) = 36 | when defined(windows): 37 | info nimDest, "RUN\nnim-" & nimVersion & "\\activate.bat" 38 | else: 39 | info nimDest, "RUN\nsource nim-" & nimVersion & "/activate.sh" 40 | 41 | proc setupNimEnv*(nimVersion: string; keepCsources: bool) = 42 | template isDevel(nimVersion: string): bool = nimVersion == "devel" 43 | 44 | template exec(command: string) = 45 | let cmd = command # eval once 46 | if os.execShellCmd(cmd) != 0: 47 | error ("nim-" & nimVersion), "failed: " & cmd 48 | return 49 | 50 | let nimDest = Path("nim-" & nimVersion) 51 | if dirExists(depsDir() / nimDest): 52 | if not fileExists(depsDir() / nimDest / ActivationFile): 53 | info nimDest, "already exists; remove or rename and try again" 54 | else: 55 | infoAboutActivation nimDest, nimVersion 56 | return 57 | 58 | var major, minor, patch: int 59 | if nimVersion != "devel": 60 | if not scanf(nimVersion, "$i.$i.$i", major, minor, patch): 61 | error "nim", "cannot parse version requirement" 62 | return 63 | let csourcesVersion = 64 | if nimVersion.isDevel or (major == 1 and minor >= 9) or major >= 2: 65 | # already uses csources_v2 66 | "csources_v2" 67 | elif major == 0: 68 | "csources" # has some chance of working 69 | else: 70 | "csources_v1" 71 | withDir $depsDir(): 72 | if not dirExists(csourcesVersion): 73 | exec "git clone https://github.com/nim-lang/" & csourcesVersion 74 | exec "git clone https://github.com/nim-lang/nim " & $nimDest 75 | withDir $depsDir() / csourcesVersion: 76 | when defined(windows): 77 | exec "build.bat" 78 | else: 79 | let makeExe = findExe("make") 80 | if makeExe.len == 0: 81 | exec "sh build.sh" 82 | else: 83 | exec "make" 84 | let nimExe0 = ".." / csourcesVersion / "bin" / "nim".addFileExt(ExeExt) 85 | let dir = Path(depsDir() / nimDest) 86 | withDir $(depsDir() / nimDest): 87 | let nimExe = "bin" / "nim".addFileExt(ExeExt) 88 | copyFileWithPermissions nimExe0, nimExe 89 | let query = createQueryEq(if nimVersion.isDevel: Version"#head" else: Version(nimVersion)) 90 | if not nimVersion.isDevel: 91 | let commit = versionToCommit(dir, SemVer, query) 92 | if commit.isEmpty(): 93 | error nimDest, "cannot resolve version to a commit" 94 | return 95 | discard checkoutGitCommit(dir, commit) 96 | exec nimExe & " c --noNimblePath --skipUserCfg --skipParentCfg --hints:off koch" 97 | let kochExe = when defined(windows): "koch.exe" else: "./koch" 98 | exec kochExe & " boot -d:release --skipUserCfg --skipParentCfg --hints:off" 99 | exec kochExe & " tools --skipUserCfg --skipParentCfg --hints:off" 100 | # remove any old atlas binary that we now would end up using: 101 | if cmpPaths(getAppDir(), $(depsDir() / nimDest / "bin".Path)) != 0: 102 | removeFile "bin" / "atlas".addFileExt(ExeExt) 103 | # unless --keep is used delete the csources because it takes up about 2GB and 104 | # is not necessary afterwards: 105 | if not keepCsources: 106 | removeDir $depsDir() / csourcesVersion / "c_code" 107 | let pathEntry = depsDir() / nimDest / "bin".Path 108 | when defined(windows): 109 | writeFile "activate.bat", BatchFile % replace($pathEntry, '/', '\\') 110 | else: 111 | writeFile "activate.sh", ShellFile % $pathEntry 112 | infoAboutActivation nimDest, nimVersion 113 | -------------------------------------------------------------------------------- /src/pkgsearch.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Atlas Package Cloner 3 | # (c) Copyright 2021 Andreas Rumpf 4 | # 5 | # See the file "copying.txt", included in this 6 | # distribution, for details about the copyright. 7 | # 8 | 9 | import std / [json, os, sets, strutils, httpclient, uri] 10 | import basic/[context, reporters, packageinfos] 11 | 12 | type PkgCandidates* = array[3, seq[PackageInfo]] 13 | 14 | proc determineCandidates*(pkgList: seq[PackageInfo]; 15 | terms: seq[string]): PkgCandidates = 16 | result[0] = @[] 17 | result[1] = @[] 18 | result[2] = @[] 19 | for pkg in pkgList: 20 | block termLoop: 21 | for term in terms: 22 | let word = term.toLower 23 | if word == pkg.name.toLower: 24 | result[0].add pkg 25 | break termLoop 26 | elif word in pkg.name.toLower: 27 | result[1].add pkg 28 | break termLoop 29 | else: 30 | for tag in pkg.tags: 31 | if word in tag.toLower: 32 | result[2].add pkg 33 | break termLoop 34 | 35 | proc singleGithubSearch(term: string, fullSearch = false): JsonNode = 36 | when UnitTests: 37 | echo "SEARCH: ", term 38 | let filename = "query_github_" & term & ".json" 39 | let path = findAtlasDir() / "tests" / "test_data" / filename 40 | result = json.parseFile(path) 41 | else: 42 | # For example: 43 | # https://api.github.com/search/repositories?q=weave+language:nim 44 | var client = newHttpClient() 45 | try: 46 | var searchUrl = "https://api.github.com/search/repositories?q=" & encodeUrl(term) 47 | if not fullSearch: 48 | searchUrl &= "+language:nim" 49 | 50 | let x = client.getContent(searchUrl) 51 | result = parseJson(x).getOrDefault("items") 52 | if result.kind != JArray: 53 | error "github search", "got bad results from GitHub" 54 | result = newJArray() 55 | # do full search and filter for languages 56 | if fullSearch: 57 | var filtered = newJArray() 58 | for item in result.items(): 59 | let queryUrl = item["languages_url"].getStr 60 | let langs = client.getContent(queryUrl).parseJson() 61 | if langs.hasKey("Nim"): 62 | filtered.add item 63 | result = filtered 64 | 65 | if result.len() == 0: 66 | if not fullSearch: 67 | trace "github search", "no results found by Github quick search; doing full search" 68 | result = singleGithubSearch(term, fullSearch=true) 69 | else: 70 | trace "github search", "no results found by Github full search" 71 | else: 72 | trace "github search", "found " & $result.len() & " results on GitHub" 73 | except CatchableError as exc: 74 | error "github search", "error searching github: " & exc.msg 75 | # result = parseJson("{\"items\": []}") 76 | result = newJArray() 77 | finally: 78 | client.close() 79 | 80 | proc githubSearch(seen: var HashSet[string]; terms: seq[string]) = 81 | for term in terms: 82 | for j in items(singleGithubSearch(term)): 83 | let p = PackageInfo( 84 | kind: pkPackage, 85 | name: j.getOrDefault("name").getStr, 86 | url: j.getOrDefault("html_url").getStr, 87 | downloadMethod: "git", 88 | tags: toTags(j.getOrDefault("topics")), 89 | description: j.getOrDefault("description").getStr, 90 | license: j.getOrDefault("license").getOrDefault("spdx_id").getStr, 91 | web: j.getOrDefault("html_url").getStr 92 | ) 93 | if not seen.containsOrIncl(p.url): 94 | echo p 95 | 96 | proc getUrlFromGithub*(term: string): string = 97 | var matches = 0 98 | result = "" 99 | for j in items(singleGithubSearch(term)): 100 | let name = j.getOrDefault("name").getStr 101 | if cmpIgnoreCase(name, term) == 0: 102 | result = j.getOrDefault("html_url").getStr 103 | inc matches 104 | if matches != 1: 105 | # ambiguous, not ok! 106 | result = "" 107 | 108 | proc search*(pkgList: seq[PackageInfo]; terms: seq[string]) = 109 | var seen = initHashSet[string]() 110 | template onFound = 111 | info "Found package", $pkg 112 | seen.incl pkg.url 113 | break forPackage 114 | 115 | for pkg in pkgList: 116 | if terms.len > 0: 117 | block forPackage: 118 | for term in terms: 119 | let word = term.toLower 120 | # Search by name. 121 | if word in pkg.name.toLower: 122 | onFound() 123 | # Search by tag. 124 | for tag in pkg.tags: 125 | if word in tag.toLower: 126 | onFound() 127 | else: 128 | info("Using package", $pkg) 129 | githubSearch seen, terms 130 | if seen.len == 0 and terms.len > 0: 131 | info("No PackageInfo found", $terms) -------------------------------------------------------------------------------- /src/runners.nim: -------------------------------------------------------------------------------- 1 | # 2 | # Atlas Package Cloner 3 | # (c) Copyright 2023 Andreas Rumpf 4 | # 5 | # See the file "copying.txt", included in this 6 | # distribution, for details about the copyright. 7 | # 8 | 9 | import basic/context 10 | 11 | import std / [strutils, os, paths, osproc] 12 | 13 | const 14 | BuilderScriptTemplate* = """ 15 | 16 | const matchedPattern = $1 17 | 18 | template builder(pattern: string; body: untyped) = 19 | when pattern == matchedPattern: 20 | body 21 | 22 | include $2 23 | """ 24 | InstallHookTemplate* = """ 25 | 26 | var 27 | packageName* = "" ## Set this to the package name. It 28 | ## is usually not required to do that, nims' filename is 29 | ## the default. 30 | version*: string ## The package's version. 31 | author*: string ## The package's author. 32 | description*: string ## The package's description. 33 | license*: string ## The package's license. 34 | srcDir*: string ## The package's source directory. 35 | binDir*: string ## The package's binary directory. 36 | backend*: string ## The package's backend. 37 | 38 | skipDirs*, skipFiles*, skipExt*, installDirs*, installFiles*, 39 | installExt*, bin*: seq[string] = @[] ## Nimble metadata. 40 | requiresData*: seq[string] = @[] ## The package's dependencies. 41 | 42 | foreignDeps*: seq[string] = @[] ## The foreign dependencies. Only 43 | ## exported for 'distros.nim'. 44 | 45 | proc requires*(deps: varargs[string]) = 46 | for d in deps: requiresData.add(d) 47 | 48 | template after(name, body: untyped) = 49 | when astToStr(name) == "install": 50 | body 51 | 52 | template before(name, body: untyped) = 53 | when astToStr(name) == "install": 54 | body 55 | 56 | proc getPkgDir*(): string = getCurrentDir() 57 | proc thisDir*(): string = getCurrentDir() 58 | 59 | include $1 60 | 61 | """ 62 | 63 | proc runNimScript*(scriptContent: string; name: string) = 64 | var buildNims = "atlas_build_0.nims" 65 | var i = 1 66 | while fileExists(buildNims): 67 | if i >= 20: 68 | error name, "could not create new: atlas_build_0.nims" 69 | return 70 | buildNims = "atlas_build_" & $i & ".nims" 71 | inc i 72 | 73 | writeFile buildNims, scriptContent 74 | 75 | let cmdLine = "nim e --hints:off -d:atlas " & quoteShell(buildNims) 76 | if os.execShellCmd(cmdLine) != 0: 77 | error name, "Nimscript failed: " & cmdLine 78 | else: 79 | removeFile buildNims 80 | 81 | proc runNimScriptInstallHook*(nimbleFile: Path, name: string) = 82 | infoNow name, "running install hooks" 83 | runNimScript InstallHookTemplate % [escape($(nimbleFile))], name 84 | 85 | proc runNimScriptBuilder*(p: (string, string); name: string) = 86 | infoNow name, "running nimble build scripts" 87 | runNimScript BuilderScriptTemplate % [p[0].escape, p[1].escape], name 88 | -------------------------------------------------------------------------------- /tests/config.nims: -------------------------------------------------------------------------------- 1 | --noNimblePath 2 | --path:"../src" 3 | --path:"$nim" -------------------------------------------------------------------------------- /tests/githttpserver.nim: -------------------------------------------------------------------------------- 1 | 2 | import asynchttpserver, asyncdispatch 3 | import os, strutils, mimetypes, httpclient 4 | import basic/context 5 | 6 | var 7 | searchDirs: seq[string] 8 | c = Reporter() 9 | 10 | proc findDir(org, repo, files: string): string = 11 | {.cast(gcsafe).}: 12 | # search for org matches first 13 | for dir in searchDirs: 14 | result = dir / org / repo / files 15 | # infoNow "searching: ", result 16 | if fileExists(result): 17 | return 18 | # otherwise try without org in the searchdir 19 | for dir in searchDirs: 20 | result = dir / repo / files 21 | # infoNow "searching: ", result 22 | if fileExists(result): 23 | return 24 | 25 | if not repo.endsWith(".git"): 26 | return findDir(org, repo & ".git", files) 27 | 28 | proc handleRequest(req: Request) {.async.} = 29 | # infoNow "http request: ", req.reqMethod, " url: ", req.url.path 30 | 31 | let arg = req.url.path.strip(chars={'/'}) 32 | var path: string 33 | try: 34 | let dirs = arg.split('/') 35 | let org = dirs[0] 36 | let repo = dirs[1] 37 | let files = dirs[2..^1].join($DirSep) 38 | path = findDir(org, repo, files) 39 | # infoNow "http repo: ", " repo: ", repo, " path: ", path 40 | except IndexDefect: 41 | {.cast(gcsafe).}: 42 | path = findDir("", "", arg) 43 | # infoNow "http direct file: ", path 44 | 45 | # Serve static files if not a git request 46 | if fileExists(path): 47 | let ext = splitFile(path).ext 48 | var contentType = newMimetypes().getMimetype(ext.strip(chars={'.'})) 49 | if contentType == "": contentType = "application/octet-stream" 50 | 51 | var headers = newHttpHeaders() 52 | headers["Content-Type"] = contentType 53 | 54 | let content = readFile(path) 55 | await req.respond(Http200, content, headers) 56 | else: 57 | await req.respond(Http404, "File not found") 58 | 59 | proc runGitHttpServer*(dirs: seq[string], port = Port(4242)) = 60 | {.cast(gcsafe).}: 61 | for dir in dirs: 62 | let d = dir.absolutePath() 63 | if not dirExists(d): 64 | raise newException(ValueError, "directory not found: " & d) 65 | searchDirs.add(d) 66 | let server = newAsyncHttpServer() 67 | doAssert searchDirs.len() >= 1, "must provide at least one directory to serve repos from" 68 | infoNow "githttpserver", "Starting http git server on port " & repr(port) 69 | infoNow "githttpserver", "Git http server serving directories: " 70 | for sd in searchDirs: 71 | infoNow "githttpserver", "\t" & sd 72 | waitFor server.serve(port, handleRequest) 73 | 74 | proc threadGitHttpServer*(args: (seq[string], Port)) {.thread.} = 75 | let dirs = args[0] 76 | let port = args[1] 77 | runGitHttpServer(dirs, port) 78 | 79 | var thread: Thread[(seq[string], Port)] 80 | proc runGitHttpServerThread*(dirs: openArray[string], port = Port(4242)) = 81 | let dirs = @dirs 82 | createThread(thread, threadGitHttpServer, (dirs, port)) 83 | 84 | proc checkHttpReadme*(): bool = 85 | let client = newHttpClient() 86 | let response = client.get("http://localhost:4242/readme.md") 87 | infoNow "githttpserver", "HTTP Server gave response: " & response.body 88 | response.body == "This directory holds the bare git modules used for testing." 89 | 90 | when isMainModule: 91 | var dirs: seq[string] 92 | runGitHttpServer(commandLineParams()) 93 | -------------------------------------------------------------------------------- /tests/tbasics.nims: -------------------------------------------------------------------------------- 1 | --noNimblePath 2 | --path:"../src" 3 | --path:"$nim" 4 | --d:atlasStandAlone 5 | --d:atlasUnitTests -------------------------------------------------------------------------------- /tests/testFeatures.nim: -------------------------------------------------------------------------------- 1 | # Small program that runs the test cases 2 | 3 | import std / [strutils, os, uri, jsonutils, json, tables, sequtils, sets, unittest] 4 | import std/terminal 5 | 6 | import basic/[sattypes, context, reporters, pkgurls, compiledpatterns, versions] 7 | import basic/[deptypes, nimblecontext, deptypesjson] 8 | import dependencies 9 | import depgraphs 10 | import testerutils 11 | import atlas, confighandler 12 | 13 | ensureGitHttpServer() 14 | 15 | proc setupProjTest() = 16 | withDir "deps" / "proj_a": 17 | writeFile("proj_a.nimble", dedent""" 18 | requires "proj_b >= 1.1.0" 19 | feature "testing": 20 | requires "proj_feature_dep >= 1.0.0" 21 | """) 22 | exec "git commit -a -m \"feat: add proj_a.nimble\"" 23 | exec "git tag v1.2.0" 24 | 25 | removeDir "proj_feature_dep" 26 | createDir "proj_feature_dep" 27 | withDir "proj_feature_dep": 28 | writeFile("proj_feature_dep.nimble", dedent""" 29 | version "1.0.0" 30 | """) 31 | exec "git init" 32 | exec "git add proj_feature_dep.nimble" 33 | exec "git commit -m \"feat: add proj_feature_dep.nimble\"" 34 | exec "git tag v1.0.0" 35 | 36 | suite "test features": 37 | setup: 38 | # setAtlasVerbosity(Trace) 39 | context().nameOverrides = Patterns() 40 | context().urlOverrides = Patterns() 41 | context().proxy = parseUri "http://localhost:4242" 42 | context().flags.incl DumbProxy 43 | context().depsDir = Path "deps" 44 | setAtlasErrorsColor(fgMagenta) 45 | 46 | test "setup and test target project": 47 | # setAtlasVerbosity(Info) 48 | setAtlasVerbosity(Error) 49 | withDir "tests/ws_features": 50 | removeDir("deps") 51 | project(paths.getCurrentDir()) 52 | context().flags = {ListVersions} 53 | context().defaultAlgo = SemVer 54 | 55 | expectedVersionWithGitTags() 56 | var nc = createNimbleContext() 57 | nc.put("proj_a", toPkgUriRaw(parseUri "https://example.com/buildGraph/proj_a", true)) 58 | nc.put("proj_b", toPkgUriRaw(parseUri "https://example.com/buildGraph/proj_b", true)) 59 | nc.put("proj_c", toPkgUriRaw(parseUri "https://example.com/buildGraph/proj_c", true)) 60 | nc.put("proj_d", toPkgUriRaw(parseUri "https://example.com/buildGraph/proj_d", true)) 61 | # nc.put("proj_feature_dep", toPkgUriRaw(parseUri "https://example.com/buildGraph/proj_feature_dep", true)) 62 | nc.put("proj_feature_dep", toPkgUriRaw(parseUri "file://" & (ospaths2.getCurrentDir() / "proj_feature_dep").absolutePath, true)) 63 | 64 | check nc.lookup("proj_a").hasShortName 65 | check nc.lookup("proj_a").projectName == "proj_a" 66 | 67 | let dir = paths.getCurrentDir().absolutePath 68 | 69 | var graph0 = dir.loadWorkspace(nc, AllReleases, onClone=DoClone, doSolve=false) 70 | writeDepGraph(graph0) 71 | 72 | setupProjTest() 73 | 74 | test "setup and test target project": 75 | # setAtlasVerbosity(Info) 76 | setAtlasVerbosity(Trace) 77 | withDir "tests/ws_features": 78 | # removeDir("deps") 79 | project(paths.getCurrentDir()) 80 | context().flags = {ListVersions} 81 | context().defaultAlgo = SemVer 82 | context().flags.incl DumpFormular 83 | 84 | expectedVersionWithGitTags() 85 | var nc = createNimbleContext() 86 | nc.put("proj_a", toPkgUriRaw(parseUri "https://example.com/buildGraph/proj_a", true)) 87 | nc.put("proj_b", toPkgUriRaw(parseUri "https://example.com/buildGraph/proj_b", true)) 88 | nc.put("proj_c", toPkgUriRaw(parseUri "https://example.com/buildGraph/proj_c", true)) 89 | nc.put("proj_d", toPkgUriRaw(parseUri "https://example.com/buildGraph/proj_d", true)) 90 | # nc.put("proj_feature_dep", toPkgUriRaw(parseUri "deps/proj_feature_dep_git", true)) 91 | nc.put("proj_feature_dep", toPkgUriRaw(parseUri "file://" & (ospaths2.getCurrentDir() / "proj_feature_dep").absolutePath, true)) 92 | 93 | check nc.lookup("proj_a").hasShortName 94 | check nc.lookup("proj_a").projectName == "proj_a" 95 | 96 | let dir = paths.getCurrentDir().absolutePath 97 | 98 | var graph = dir.loadWorkspace(nc, AllReleases, onClone=DoClone, doSolve=true) 99 | writeDepGraph(graph) 100 | 101 | # checkpoint "\tgraph:\n" & $graph.toJson(ToJsonOptions(enumMode: joptEnumString)) 102 | 103 | # check false 104 | 105 | # let form = graph.toFormular(SemVer) 106 | # context().flags.incl DumpGraphs 107 | # var sol: Solution 108 | # solve(graph, form) 109 | 110 | check graph.root.active 111 | check graph.pkgs[nc.createUrl("proj_a")].active 112 | check graph.pkgs[nc.createUrl("proj_b")].active 113 | check graph.pkgs[nc.createUrl("proj_c")].active 114 | check graph.pkgs[nc.createUrl("proj_d")].active 115 | check graph.pkgs[nc.createUrl("proj_feature_dep")].active 116 | 117 | check $graph.root.activeVersion == "#head@-" 118 | # check $graph.pkgs[nc.createUrl("proj_a")].activeVersion == $findCommit("proj_a", "1.1.0") 119 | check $graph.pkgs[nc.createUrl("proj_a")].activeVersion.vtag.version == "1.2.0" 120 | check $graph.pkgs[nc.createUrl("proj_b")].activeVersion == $findCommit("proj_b", "1.1.0") 121 | check $graph.pkgs[nc.createUrl("proj_c")].activeVersion == $findCommit("proj_c", "1.2.0") 122 | check $graph.pkgs[nc.createUrl("proj_d")].activeVersion == $findCommit("proj_d", "1.0.0") 123 | check $graph.pkgs[nc.createUrl("proj_feature_dep")].activeVersion.vtag.version == "1.0.0" 124 | 125 | # let graph2 = loadJson("graph-solved.json") 126 | 127 | let jnRoot = toJson(graph.root) 128 | var graphRoot: Package 129 | graphRoot.fromJson(jnRoot) 130 | echo "graphRoot: ", $graphRoot.toJson(ToJsonOptions(enumMode: joptEnumString)) 131 | 132 | # check graph.toJson(ToJsonOptions(enumMode: joptEnumString)) == graph2.toJson(ToJsonOptions(enumMode: joptEnumString)) 133 | 134 | suite "test global features": 135 | setup: 136 | # setAtlasVerbosity(Trace) 137 | context().nameOverrides = Patterns() 138 | context().urlOverrides = Patterns() 139 | context().proxy = parseUri "http://localhost:4242" 140 | context().flags.incl DumbProxy 141 | context().depsDir = Path "deps" 142 | setAtlasErrorsColor(fgMagenta) 143 | 144 | test "setup and test target project": 145 | # setAtlasVerbosity(Info) 146 | setAtlasVerbosity(Error) 147 | withDir "tests/ws_features_global": 148 | removeDir("deps") 149 | project(paths.getCurrentDir()) 150 | context().flags = {ListVersions} 151 | context().defaultAlgo = SemVer 152 | 153 | expectedVersionWithGitTags() 154 | var nc = createNimbleContext() 155 | nc.put("proj_a", toPkgUriRaw(parseUri "https://example.com/buildGraph/proj_a", true)) 156 | nc.put("proj_b", toPkgUriRaw(parseUri "https://example.com/buildGraph/proj_b", true)) 157 | nc.put("proj_c", toPkgUriRaw(parseUri "https://example.com/buildGraph/proj_c", true)) 158 | nc.put("proj_d", toPkgUriRaw(parseUri "https://example.com/buildGraph/proj_d", true)) 159 | # nc.put("proj_feature_dep", toPkgUriRaw(parseUri "https://example.com/buildGraph/proj_feature_dep", true)) 160 | nc.put("proj_feature_dep", toPkgUriRaw(parseUri "file://" & (ospaths2.getCurrentDir() / "proj_feature_dep").absolutePath, true)) 161 | 162 | check nc.lookup("proj_a").hasShortName 163 | check nc.lookup("proj_a").projectName == "proj_a" 164 | 165 | let dir = paths.getCurrentDir().absolutePath 166 | 167 | var graph0 = dir.loadWorkspace(nc, AllReleases, onClone=DoClone, doSolve=false) 168 | writeDepGraph(graph0) 169 | 170 | setupProjTest() 171 | 172 | test "setup and test target project": 173 | # setAtlasVerbosity(Info) 174 | setAtlasVerbosity(Trace) 175 | withDir "tests/ws_features_global": 176 | # removeDir("deps") 177 | project(paths.getCurrentDir()) 178 | context().flags = {ListVersions} 179 | context().defaultAlgo = SemVer 180 | context().flags.incl DumpFormular 181 | context().features.incl "feature.proj_a.testing" 182 | 183 | expectedVersionWithGitTags() 184 | var nc = createNimbleContext() 185 | nc.put("proj_a", toPkgUriRaw(parseUri "https://example.com/buildGraph/proj_a", true)) 186 | nc.put("proj_b", toPkgUriRaw(parseUri "https://example.com/buildGraph/proj_b", true)) 187 | nc.put("proj_c", toPkgUriRaw(parseUri "https://example.com/buildGraph/proj_c", true)) 188 | nc.put("proj_d", toPkgUriRaw(parseUri "https://example.com/buildGraph/proj_d", true)) 189 | # nc.put("proj_feature_dep", toPkgUriRaw(parseUri "deps/proj_feature_dep_git", true)) 190 | nc.put("proj_feature_dep", toPkgUriRaw(parseUri "file://" & (ospaths2.getCurrentDir() / "proj_feature_dep").absolutePath, true)) 191 | 192 | check nc.lookup("proj_a").hasShortName 193 | check nc.lookup("proj_a").projectName == "proj_a" 194 | 195 | let dir = paths.getCurrentDir().absolutePath 196 | 197 | var graph = dir.loadWorkspace(nc, AllReleases, onClone=DoClone, doSolve=true) 198 | writeDepGraph(graph) 199 | 200 | # checkpoint "\tgraph:\n" & $graph.toJson(ToJsonOptions(enumMode: joptEnumString)) 201 | 202 | # check false 203 | 204 | # let form = graph.toFormular(SemVer) 205 | # context().flags.incl DumpGraphs 206 | # var sol: Solution 207 | # solve(graph, form) 208 | 209 | check graph.root.active 210 | check graph.pkgs[nc.createUrl("proj_a")].active 211 | check graph.pkgs[nc.createUrl("proj_b")].active 212 | check graph.pkgs[nc.createUrl("proj_c")].active 213 | check graph.pkgs[nc.createUrl("proj_d")].active 214 | check nc.createUrl("proj_feature_dep") in graph.pkgs 215 | check graph.pkgs[nc.createUrl("proj_feature_dep")].active 216 | 217 | check $graph.root.activeVersion == "#head@-" 218 | # check $graph.pkgs[nc.createUrl("proj_a")].activeVersion == $findCommit("proj_a", "1.1.0") 219 | check $graph.pkgs[nc.createUrl("proj_a")].activeVersion.vtag.version == "1.2.0" 220 | check $graph.pkgs[nc.createUrl("proj_b")].activeVersion == $findCommit("proj_b", "1.1.0") 221 | check $graph.pkgs[nc.createUrl("proj_c")].activeVersion == $findCommit("proj_c", "1.2.0") 222 | check $graph.pkgs[nc.createUrl("proj_d")].activeVersion == $findCommit("proj_d", "1.0.0") 223 | check $graph.pkgs[nc.createUrl("proj_feature_dep")].activeVersion.vtag.version == "1.0.0" 224 | 225 | # let graph2 = loadJson("graph-solved.json") 226 | 227 | let jnRoot = toJson(graph.root) 228 | var graphRoot: Package 229 | graphRoot.fromJson(jnRoot) 230 | echo "graphRoot: ", $graphRoot.toJson(ToJsonOptions(enumMode: joptEnumString)) 231 | 232 | # check graph.toJson(ToJsonOptions(enumMode: joptEnumString)) == graph2.toJson(ToJsonOptions(enumMode: joptEnumString)) 233 | -------------------------------------------------------------------------------- /tests/test_data/bad.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | version = '0.8.0' 3 | description = "Atlas is a simple package cloner tool. It manages an isolated project." 4 | license = "MIT" 5 | srcDir = "src' 6 | 7 | # Dependencies 8 | 9 | requires "nim >= 2.0.0" 10 | -------------------------------------------------------------------------------- /tests/test_data/jester.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.6.0" # Be sure to update jester.jesterVer too! 4 | author = "Dominik Picheta" 5 | description = "A sinatra-like web framework for Nim." 6 | license = "MIT" 7 | 8 | skipFiles = @["todo.markdown"] 9 | skipDirs = @["tests"] 10 | 11 | # Deps 12 | 13 | requires "nim >= 1.0.0" 14 | 15 | when not defined(windows): 16 | requires "httpbeast >= 0.4.0" 17 | 18 | task test, "Runs the test suite.": 19 | exec "nimble install -y asynctools@#0e6bdc3ed5bae8c7cc9" 20 | exec "nim c -r tests/tester" 21 | -------------------------------------------------------------------------------- /tests/test_data/jester_boolean.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.6.0" # Be sure to update jester.jesterVer too! 4 | author = "Dominik Picheta" 5 | description = "A sinatra-like web framework for Nim." 6 | license = "MIT" 7 | 8 | skipFiles = @["todo.markdown"] 9 | skipDirs = @["tests"] 10 | 11 | # Deps 12 | 13 | requires "nim >= 1.0.0" 14 | 15 | when defined(linux): 16 | requires "httpbeast >= 0.1.0" 17 | 18 | when defined(linux) or defined(macosx): 19 | requires "httpbeast >= 0.2.0" 20 | 21 | when not defined(linux) or defined(macosx): 22 | requires "httpbeast >= 0.3.0" 23 | 24 | when not (defined(linux) or defined(macosx)): 25 | requires "httpbeast >= 0.4.0" 26 | 27 | when defined(windows) and (defined(linux) or defined(macosx)): 28 | requires "httpbeast >= 0.5.0" 29 | 30 | task test, "Runs the test suite.": 31 | exec "nimble install -y asynctools@#0e6bdc3ed5bae8c7cc9" 32 | exec "nim c -r tests/tester" 33 | -------------------------------------------------------------------------------- /tests/test_data/jester_combined.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.6.0" # Be sure to update jester.jesterVer too! 4 | author = "Dominik Picheta" 5 | description = "A sinatra-like web framework for Nim." 6 | license = "MIT" 7 | 8 | skipFiles = @["todo.markdown"] 9 | skipDirs = @["tests"] 10 | 11 | # Deps 12 | 13 | requires "nim >= 1.0.0" 14 | 15 | when defined(linux) or defined(macosx): 16 | requires "httpbeast >= 0.4.0" 17 | 18 | task test, "Runs the test suite.": 19 | exec "nimble install -y asynctools@#0e6bdc3ed5bae8c7cc9" 20 | exec "nim c -r tests/tester" 21 | -------------------------------------------------------------------------------- /tests/test_data/jester_feature.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.6.0" # Be sure to update jester.jesterVer too! 4 | author = "Dominik Picheta" 5 | description = "A sinatra-like web framework for Nim." 6 | license = "MIT" 7 | 8 | skipFiles = @["todo.markdown"] 9 | skipDirs = @["tests"] 10 | 11 | # Deps 12 | 13 | requires "nim >= 1.0.0" 14 | 15 | feature "useHttpbeast": 16 | requires "httpbeast >= 0.4.0" 17 | 18 | feature "useAsyncTools", "useOldAsyncTools": 19 | requires "asynctools >= 0.1.0" 20 | 21 | task test, "Runs the test suite.": 22 | exec "nimble install -y asynctools@#0e6bdc3ed5bae8c7cc9" 23 | exec "nim c -r tests/tester" 24 | -------------------------------------------------------------------------------- /tests/test_data/jester_inverted.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.6.0" # Be sure to update jester.jesterVer too! 4 | author = "Dominik Picheta" 5 | description = "A sinatra-like web framework for Nim." 6 | license = "MIT" 7 | 8 | skipFiles = @["todo.markdown"] 9 | skipDirs = @["tests"] 10 | 11 | # Deps 12 | 13 | requires "nim >= 1.0.0" 14 | 15 | when defined(linux): 16 | requires "httpbeast >= 0.4.0" 17 | when defined(macosx): 18 | requires "httpbeast >= 0.4.0" 19 | 20 | task test, "Runs the test suite.": 21 | exec "nimble install -y asynctools@#0e6bdc3ed5bae8c7cc9" 22 | exec "nim c -r tests/tester" 23 | -------------------------------------------------------------------------------- /tests/tester.nim: -------------------------------------------------------------------------------- 1 | # Small program that runs the test cases 2 | 3 | import std / [strutils, os, osproc, sequtils, strformat, unittest] 4 | import basic/context 5 | import testerutils 6 | 7 | import testintegration 8 | 9 | infoNow "tester", "All tests run successfully" 10 | -------------------------------------------------------------------------------- /tests/testerutils.nim: -------------------------------------------------------------------------------- 1 | import std / [os, strutils, sequtils] 2 | from std/private/gitutils import diffFiles 3 | import basic/versions 4 | export diffFiles 5 | import githttpserver 6 | 7 | let atlasExe* = absolutePath("bin" / "atlas".addFileExt(ExeExt)) 8 | 9 | proc exec*(cmd: string) = 10 | if execShellCmd(cmd) != 0: 11 | quit "FAILURE RUNNING: " & cmd 12 | 13 | template withDir*(dir: string; body: untyped) = 14 | let old = getCurrentDir() 15 | try: 16 | setCurrentDir(dir) 17 | # echo "WITHDIR: ", dir, " at: ", getCurrentDir() 18 | body 19 | finally: 20 | setCurrentDir(old) 21 | 22 | template sameDirContents*(expected, given: string) = 23 | # result = true 24 | for _, e in walkDir(expected): 25 | let g = given / splitPath(e).tail 26 | if fileExists(g): 27 | let edata = readFile(e) 28 | let gdata = readFile(g) 29 | check gdata == edata 30 | if gdata != edata: 31 | echo "FAILURE: files differ: ", e.absolutePath, " to: ", g.absolutePath 32 | echo diffFiles(e, g).output 33 | else: 34 | echo "SUCCESS: files match: ", e.absolutePath 35 | else: 36 | echo "FAILURE: file does not exist: ", g 37 | check fileExists(g) 38 | # result = false 39 | 40 | proc ensureGitHttpServer*() = 41 | try: 42 | if checkHttpReadme(): 43 | return 44 | except CatchableError: 45 | echo "Starting Tester git http server" 46 | runGitHttpServerThread([ 47 | "atlas-tests/ws_integration", 48 | "atlas-tests/ws_generated" 49 | ]) 50 | for count in 1..10: 51 | os.sleep(1000) 52 | if checkHttpReadme(): 53 | return 54 | 55 | quit "Error accessing git-http server.\n" & 56 | "Check that tests/githttpserver server is running on port 4242.\n" & 57 | "To start it run in another terminal:\n" & 58 | " nim c -r tests/githttpserver test-repos/generated" 59 | 60 | template expectedVersionWithGitTags*() = 61 | # These will change if atlas-tests is regnerated! 62 | # To update run and use commits not adding a proj_x.nim file 63 | # curl http://localhost:4242/buildGraph/ws_generated-logs.txt 64 | 65 | # note: the middle commit is where nimble file comment is changed but the version is the same 66 | let projAnimbles {.inject.} = dedent""" 67 | b62ef0ae3e5d888e28f432d87645ee945ced6f19 1.1.0 68 | 7f5302d5ea45c5c040d2939ef59449e801d59054 69 | 58ea44fb5cf98b7d333fd482dbccea9dd82050ff 1.0.0 70 | """.parseTaggedVersions(false) 71 | let projAtags {.inject.} = projAnimbles.filterIt(it.v.string != "") 72 | 73 | # note: the middle commit is where nimble file comment is changed but the version is the same 74 | let projBnimbles {.inject.} = dedent""" 75 | 423774bee431e28321989eb50a9ca70650986088 1.1.0 76 | fa97674802701849b4ec488aeb20019b5e843510 77 | 1f2221a7186c65d588e4cdf46dba7bb46a1c90a5 1.0.0 78 | """.parseTaggedVersions(false) 79 | let projBtags {.inject.} = projBnimbles.filterIt(it.v.string != "") 80 | 81 | let projCnimbles {.inject.} = dedent""" 82 | 7538587e7e6d7c50f6533d0d226d5ae73b91d045 1.2.0 83 | 8ba310d7c4931fc2b8ffba8d2e4c52fd0646ad73 84 | """.parseTaggedVersions(false) 85 | let projCtags {.inject.} = projCnimbles.filterIt(it.v.string != "") 86 | 87 | let projDnimbles {.inject.} = dedent""" 88 | db336b9131bde8adf4b58513b9589e89b6590893 2.0.0 89 | 1dcdfddd2aa193804286681f6ebd09e8b8b398fc 1.0.0 90 | """.parseTaggedVersions(false) 91 | let projDtags {.inject.} = projDnimbles.filterIt(it.v.string != "") 92 | 93 | template expectedVersionWithNoGitTags*() = 94 | # These will change if atlas-tests is regnerated! 95 | # To update run and use commits not adding a proj_x.nim file 96 | # curl http://localhost:4242/buildGraphNoGitTags/ws_generated-logs.txt 97 | 98 | # note: the middle commit is where nimble file comment is changed but the version is the same 99 | let projAnimbles {.inject.} = dedent""" 100 | 2a630f98c20f54b828f95e824e2a1b2da50fe687 1.1.0 101 | 62fca4fd4062087d937146fd5d8f9ab7e1e5c22b 102 | 4d2bb051f45a3a4612f9b461401982f48e6637d7 1.0.0 103 | """.parseTaggedVersions(false) 104 | let projAtags {.inject.} = projAnimbles.filterIt(it.v.string != "") 105 | 106 | # note: the middle commit is where nimble file comment is changed but the version is the same 107 | let projBnimbles {.inject.} = dedent""" 108 | 05b2f46caf8ae7322c855a482ad297c399b5d185 1.1.0 109 | 4cee9aed9623f4142a7b15bb96a1d582c8b87250 110 | 60c3613e16d54f170e991eed3f5b23dbf1c03cf4 1.0.0 111 | """.parseTaggedVersions(false) 112 | let projBtags {.inject.} = projBnimbles.filterIt(it.v.string != "") 113 | 114 | let projCnimbles {.inject.} = dedent""" 115 | d2056e869dfcea4ed6a5fbf905e6e1922f0637c3 1.2.0 116 | 07d8c752b1810542f6e72a12eb2d26b81ee09041 1.0.0 117 | """.parseTaggedVersions(false) 118 | let projCtags {.inject.} = projCnimbles.filterIt(it.v.string != "") 119 | 120 | let projDnimbles {.inject.} = dedent""" 121 | e60f0846cc949055dc5ed4b1e65ff8bd61d17fc3 2.0.0 122 | 4ec5f558465498358096dbc6d7dd3bbedf1ef2bc 1.0.0 123 | """.parseTaggedVersions(false) 124 | let projDtags {.inject.} = projDnimbles.filterIt(it.v.string != "") 125 | 126 | template expectedVersionWithNoGitTagsMaxVer*() = 127 | # These will change if atlas-tests is regnerated! 128 | # To update run and use commits not adding a proj_x.nim file 129 | # curl http://localhost:4242/buildGraphNoGitTags/ws_generated-logs.txt 130 | 131 | # note: this variant uses the last commit where a given nimble version was found 132 | # when the nimble file was changed, the version was the same 133 | 134 | let projAnimbles {.inject.} = dedent""" 135 | 2a630f98c20f54b828f95e824e2a1b2da50fe687 1.1.0 136 | 62fca4fd4062087d937146fd5d8f9ab7e1e5c22b 1.0.0 137 | """.parseTaggedVersions(false) 138 | let projAtags {.inject.} = projAnimbles.filterIt(it.v.string != "") 139 | 140 | let projBnimbles {.inject.} = dedent""" 141 | 05b2f46caf8ae7322c855a482ad297c399b5d185 1.1.0 142 | 4cee9aed9623f4142a7b15bb96a1d582c8b87250 1.0.0 143 | """.parseTaggedVersions(false) 144 | let projBtags {.inject.} = projBnimbles.filterIt(it.v.string != "") 145 | 146 | let projCnimbles {.inject.} = dedent""" 147 | d2056e869dfcea4ed6a5fbf905e6e1922f0637c3 1.2.0 148 | 07d8c752b1810542f6e72a12eb2d26b81ee09041 1.0.0 149 | """.parseTaggedVersions(false) 150 | let projCtags {.inject.} = projCnimbles.filterIt(it.v.string != "") 151 | 152 | let projDnimbles {.inject.} = dedent""" 153 | e60f0846cc949055dc5ed4b1e65ff8bd61d17fc3 2.0.0 154 | 4ec5f558465498358096dbc6d7dd3bbedf1ef2bc 1.0.0 155 | """.parseTaggedVersions(false) 156 | let projDtags {.inject.} = projDnimbles.filterIt(it.v.string != "") 157 | 158 | template findCommit*(proj: string, version: string): VersionTag = 159 | block: 160 | var res: VersionTag 161 | case proj: 162 | of "proj_a": 163 | for idx, vt in projAnimbles: 164 | if $vt.v == version: 165 | res = vt 166 | if idx == 0: res.isTip = true 167 | of "proj_b": 168 | for idx, vt in projBnimbles: 169 | if $vt.v == version: 170 | res = vt 171 | if idx == 0: res.isTip = true 172 | of "proj_c": 173 | for idx, vt in projCnimbles: 174 | if $vt.v == version: 175 | res = vt 176 | if idx == 0: res.isTip = true 177 | of "proj_d": 178 | for idx, vt in projDnimbles: 179 | if $vt.v == version: 180 | res = vt 181 | if idx == 0: res.isTip = true 182 | else: 183 | discard 184 | res 185 | 186 | 187 | when isMainModule: 188 | expectedVersionWithGitTags() 189 | echo findCommit("proj_a", "1.1.0") 190 | echo findCommit("proj_a", "1.0.0") 191 | assert findCommit("proj_a", "1.1.0").isTip 192 | assert not findCommit("proj_a", "1.0.0").isTip -------------------------------------------------------------------------------- /tests/testintegration.nim: -------------------------------------------------------------------------------- 1 | # Small program that runs the test cases 2 | 3 | import std / [strutils, os, osproc, sequtils, strformat, unittest] 4 | import basic/context 5 | import testerutils 6 | 7 | ensureGitHttpServer() 8 | 9 | if execShellCmd("nim c -o:$# -d:release src/atlas.nim" % [atlasExe]) != 0: 10 | quit("FAILURE: compilation of atlas failed") 11 | 12 | proc integrationTest() = 13 | # Test installation of some "important_packages" which we are sure 14 | # won't disappear in the near or far future. Turns out `nitter` has 15 | # quite some dependencies so it suffices: 16 | let args = " --proxy=http://localhost:4242/ --dumbproxy --dumpgraphs --full --verbosity:info --keepWorkspace " 17 | 18 | let cmd = atlasExe & args & " install" 19 | echo "Running: ", cmd 20 | let res = execShellCmd cmd 21 | # exec atlasExe & " --verbosity:trace --keepWorkspace use https://github.com/zedeus/nitter" 22 | block: 23 | let cmd = atlasExe & args & " pin" 24 | let res = execShellCmd cmd 25 | 26 | when not defined(windows): # windows is different 27 | sameDirContents("expected", ".") 28 | 29 | if res != 0: 30 | quit "FAILURE RUNNING: " & cmd 31 | 32 | proc cleanupIntegrationTest() = 33 | var dirs: seq[string] = @[] 34 | for k, f in walkDir("."): 35 | if k == pcDir and dirExists(f / ".git"): 36 | dirs.add f 37 | for d in dirs: 38 | echo "Removing dir: ", d.absolutePath 39 | removeDir d 40 | removeFile "nim.cfg" 41 | echo "Removing configs" 42 | 43 | withDir "tests/ws_integration": 44 | when not defined(keepTestDirs): 45 | cleanupIntegrationTest() 46 | integrationTest() 47 | 48 | # if failures > 0: quit($failures & " failures occurred.") 49 | 50 | # Normal: create or remotely cloning repos 51 | # nim c -r 1.80s user 0.71s system 60% cpu 4.178 total 52 | # shims/nim c -r 32.00s user 25.11s system 41% cpu 2:18.60 total 53 | # nim c -r 30.83s user 24.67s system 40% cpu 2:17.17 total 54 | 55 | # Local repos: 56 | # nim c -r 1.59s user 0.60s system 88% cpu 2.472 total 57 | # w/integration: nim c -r 23.86s user 18.01s system 71% cpu 58.225 total 58 | # w/integration: nim c -r 32.00s user 25.11s system 41% cpu 1:22.80 total 59 | -------------------------------------------------------------------------------- /tests/testlinkintegration.nim: -------------------------------------------------------------------------------- 1 | # Small program that runs the test cases 2 | 3 | import std / [strutils, os, uri, jsonutils, json, tables, sequtils, unittest] 4 | import std/terminal 5 | 6 | import basic/[sattypes, context, reporters, pkgurls, compiledpatterns, versions] 7 | import basic/[deptypes, nimblecontext, deptypesjson] 8 | import dependencies 9 | import depgraphs 10 | import testerutils 11 | import atlas, confighandler 12 | 13 | ensureGitHttpServer() 14 | 15 | proc setupGraph*(): seq[string] = 16 | let projs = @["proj_a", "proj_b", "proj_c", "proj_d"] 17 | if not dirExists("buildGraph"): 18 | createDir "buildGraph" 19 | withDir "buildGraph": 20 | for proj in projs: 21 | exec("git clone http://localhost:4242/buildGraph/$1" % [proj]) 22 | for proj in projs: 23 | result.add(ospaths2.getCurrentDir() / "buildGraph" / proj) 24 | 25 | proc setupGraphNoGitTags*(): seq[string] = 26 | let projs = @["proj_a", "proj_b", "proj_c", "proj_d"] 27 | if not dirExists("buildGraphNoGitTags"): 28 | createDir "buildGraphNoGitTags" 29 | withDir "buildGraphNoGitTags": 30 | for proj in projs: 31 | exec("git clone http://localhost:4242/buildGraphNoGitTags/$1" % [proj]) 32 | for proj in projs: 33 | result.add(ospaths2.getCurrentDir() / "buildGraphNoGitTags" / proj) 34 | 35 | suite "test link integration": 36 | setup: 37 | # setAtlasVerbosity(Trace) 38 | context().nameOverrides = Patterns() 39 | context().urlOverrides = Patterns() 40 | context().proxy = parseUri "http://localhost:4242" 41 | context().flags.incl DumbProxy 42 | context().depsDir = Path "deps" 43 | setAtlasErrorsColor(fgMagenta) 44 | 45 | test "setup and test target project": 46 | # setAtlasVerbosity(Info) 47 | setAtlasVerbosity(Error) 48 | withDir "tests/ws_link_semver": 49 | removeDir("deps") 50 | project(paths.getCurrentDir()) 51 | context().flags = {ListVersions} 52 | context().defaultAlgo = SemVer 53 | 54 | expectedVersionWithGitTags() 55 | var nc = createNimbleContext() 56 | nc.put("proj_a", toPkgUriRaw(parseUri "https://example.com/buildGraph/proj_a", true)) 57 | nc.put("proj_b", toPkgUriRaw(parseUri "https://example.com/buildGraph/proj_b", true)) 58 | nc.put("proj_c", toPkgUriRaw(parseUri "https://example.com/buildGraph/proj_c", true)) 59 | nc.put("proj_d", toPkgUriRaw(parseUri "https://example.com/buildGraph/proj_d", true)) 60 | 61 | check nc.lookup("proj_a").hasShortName 62 | check nc.lookup("proj_a").projectName == "proj_a" 63 | 64 | let dir = paths.getCurrentDir().absolutePath 65 | 66 | var graph = dir.loadWorkspace(nc, AllReleases, onClone=DoClone, doSolve=true) 67 | writeDepGraph(graph) 68 | 69 | checkpoint "\tgraph:\n" & $graph.toJson(ToJsonOptions(enumMode: joptEnumString)) 70 | 71 | let form = graph.toFormular(SemVer) 72 | context().flags.incl DumpGraphs 73 | var sol: Solution 74 | solve(graph, form) 75 | 76 | check graph.root.active 77 | check graph.pkgs[nc.createUrl("proj_a")].active 78 | check graph.pkgs[nc.createUrl("proj_b")].active 79 | check graph.pkgs[nc.createUrl("proj_c")].active 80 | check graph.pkgs[nc.createUrl("proj_d")].active 81 | 82 | check $graph.root.activeVersion == "#head@-" 83 | check $graph.pkgs[nc.createUrl("proj_a")].activeVersion == $findCommit("proj_a", "1.1.0") 84 | check $graph.pkgs[nc.createUrl("proj_b")].activeVersion == $findCommit("proj_b", "1.1.0") 85 | check $graph.pkgs[nc.createUrl("proj_c")].activeVersion == $findCommit("proj_c", "1.2.0") 86 | check $graph.pkgs[nc.createUrl("proj_d")].activeVersion == $findCommit("proj_d", "1.0.0") 87 | 88 | # let graph2 = loadJson("graph-solved.json") 89 | 90 | let jnRoot = toJson(graph.root) 91 | var graphRoot: Package 92 | graphRoot.fromJson(jnRoot) 93 | echo "graphRoot: ", $graphRoot.toJson(ToJsonOptions(enumMode: joptEnumString)) 94 | 95 | # check graph.toJson(ToJsonOptions(enumMode: joptEnumString)) == graph2.toJson(ToJsonOptions(enumMode: joptEnumString)) 96 | 97 | test "expand using http urls with link files": 98 | setAtlasVerbosity(Warning) 99 | withDir "tests/ws_link_integration": 100 | removeDir("deps") 101 | project(paths.getCurrentDir()) 102 | context().flags = {ListVersions} 103 | context().defaultAlgo = SemVer 104 | 105 | createDir("deps") 106 | writeFile("deps" / "atlas.config", dedent""" 107 | { 108 | "deps": "deps", 109 | "nameOverrides": { 110 | "proj_a": "https://example.com/buildGraph/proj_a", 111 | "proj_b": "https://example.com/buildGraph/proj_b", 112 | "proj_c": "https://example.com/buildGraph/proj_c", 113 | "proj_d": "https://example.com/buildGraph/proj_d" 114 | } 115 | } 116 | """) 117 | 118 | expectedVersionWithGitTags() 119 | let dir = paths.getCurrentDir().absolutePath 120 | 121 | check project() == paths.getCurrentDir() 122 | atlasRun(@["link", "../ws_link_semver"]) 123 | 124 | test "expand using link files part 2": 125 | setAtlasVerbosity(Warning) 126 | withDir "tests/ws_link_integration": 127 | project(paths.getCurrentDir()) 128 | context().flags = {ListVersions} 129 | context().defaultAlgo = SemVer 130 | 131 | expectedVersionWithGitTags() 132 | readConfig() 133 | 134 | var nc = createNimbleContext() 135 | var graph = project().loadWorkspace(nc, AllReleases, onClone=DoClone, doSolve=true) 136 | 137 | checkpoint "\tgraph:\n" & $graph.toJson(ToJsonOptions(enumMode: joptEnumString)) 138 | 139 | let config = readConfigFile(getProjectConfig()) 140 | echo "config: ", $config 141 | check project() == paths.getCurrentDir() 142 | check config.nameOverrides.len == 5 143 | check config.nameOverrides["ws_link_semver"] == toWindowsFileUrl("link://" & $absolutePath($project() /../ "ws_link_semver" / "ws_link_semver.nimble")) 144 | 145 | # let form = graph.toFormular(SemVer) 146 | # context().flags.incl DumpGraphs 147 | # var sol: Solution 148 | # solve(graph, form) 149 | 150 | check graph.root.active 151 | check graph.pkgs[nc.createUrl("proj_a")].active 152 | check graph.pkgs[nc.createUrl("proj_b")].active 153 | check graph.pkgs[nc.createUrl("proj_c")].active 154 | check graph.pkgs[nc.createUrl("proj_d")].active 155 | 156 | check $graph.root.activeVersion == "#head@-" 157 | check $graph.pkgs[nc.createUrl("proj_a")].activeVersion == $findCommit("proj_a", "1.1.0") 158 | check $graph.pkgs[nc.createUrl("proj_b")].activeVersion == $findCommit("proj_b", "1.1.0") 159 | check $graph.pkgs[nc.createUrl("proj_c")].activeVersion == $findCommit("proj_c", "1.2.0") 160 | check $graph.pkgs[nc.createUrl("proj_d")].activeVersion == $findCommit("proj_d", "1.0.0") 161 | 162 | 163 | -------------------------------------------------------------------------------- /tests/testsemver.nim: -------------------------------------------------------------------------------- 1 | # Small program that runs the test cases 2 | 3 | import std / [strutils, os, osproc, sequtils, strformat, unittest] 4 | import basic/context 5 | import testerutils 6 | 7 | if execShellCmd("nim c -o:$# -d:release src/atlas.nim" % [atlasExe]) != 0: 8 | quit("FAILURE: compilation of atlas failed") 9 | 10 | ensureGitHttpServer() 11 | 12 | template testSemVer2(name, expected: string) = 13 | # createDir name 14 | # withDir name: 15 | block: 16 | let cmd = atlasExe & " --full --proxy=http://localhost:4242 --ignoreerrors --dumbProxy --keepWorkspace --resolver=SemVer --colors:off --list use proj_a" 17 | let (outp, status) = execCmdEx(cmd) 18 | if status == 0: 19 | checkpoint "<<<<<<<<<<<<<<<< Failed test\n" & 20 | "\nExpected contents:\n\t" & expected.replace("\n", "\n\t") & 21 | "\nInstead got:\n\t" & outp.replace("\n", "\n\t") & 22 | ">>>>>>>>>>>>>>>> Failed\n" 23 | check outp.contains expected 24 | else: 25 | echo "\n\n" 26 | echo "<<<<<<<<<<<<<<<< Failed Exec " 27 | echo "testSemVer2:command: ", cmd 28 | echo "testSemVer2:pwd: ", ospaths2.getCurrentDir() 29 | echo "testSemVer2:failed command:" 30 | echo "================ Output:\n\t" & outp.replace("\n", "\n\t") 31 | echo ">>>>>>>>>>>>>>>> failed\n" 32 | check status == 0 33 | 34 | template testMinVer(name, expected: string) = 35 | # createDir name 36 | # withDir name: 37 | block: 38 | let cmd = atlasExe & " --full --proxy=http://localhost:4242 --ignoreerrors --dumbProxy --keepWorkspace --resolver=MinVer --colors:off --list use proj_a" 39 | let (outp, status) = execCmdEx(cmd) 40 | if status == 0: 41 | checkpoint "<<<<<<<<<<<<<<<< Failed test\n" & 42 | "\nExpected contents:\n\t" & expected.replace("\n", "\n\t") & 43 | "\nInstead got:\n\t" & outp.replace("\n", "\n\t") & 44 | ">>>>>>>>>>>>>>>> Failed\n" 45 | check outp.contains expected 46 | else: 47 | echo "\n\n" 48 | echo "<<<<<<<<<<<<<<<< Failed Exec " 49 | echo "tesMinVer2:command: ", cmd 50 | echo "tesMinVer2:pwd: ", ospaths2.getCurrentDir() 51 | echo "tesMinVer2:failed command:" 52 | echo "================ Output:\n\t" & outp.replace("\n", "\n\t") 53 | echo ">>>>>>>>>>>>>>>> failed\n" 54 | check status == 0 55 | 56 | template removeDirs(projDir: string) = 57 | removeDir projDir 58 | removeDir "does_not_exist" 59 | # removeDir "semproject" 60 | # removeDir "minproject" 61 | removeDir "source" 62 | removeDir "proj_a" 63 | removeDir "proj_b" 64 | removeDir "proj_c" 65 | removeDir "proj_d" 66 | 67 | proc setupGraph* = 68 | createDir "source" 69 | withDir "source": 70 | 71 | exec "git clone http://localhost:4242/buildGraph/proj_a" 72 | exec "git clone http://localhost:4242/buildGraph/proj_b" 73 | exec "git clone http://localhost:4242/buildGraph/proj_c" 74 | exec "git clone http://localhost:4242/buildGraph/proj_d" 75 | 76 | proc setupGraphNoGitTags* = 77 | createDir "source" 78 | withDir "source": 79 | 80 | exec "git clone http://localhost:4242/buildGraphNoGitTags/proj_a" 81 | exec "git clone http://localhost:4242/buildGraphNoGitTags/proj_b" 82 | exec "git clone http://localhost:4242/buildGraphNoGitTags/proj_c" 83 | exec "git clone http://localhost:4242/buildGraphNoGitTags/proj_d" 84 | 85 | suite "basic repo tests": 86 | test "semproject1": 87 | withDir "tests/ws_semproject1": 88 | removeDirs("deps") 89 | setupGraph() 90 | let semVerExpectedResult = dedent""" 91 | [Warn] (Resolved) selected: 92 | [Warn] (proj_a.buildGraph.example.com) [ ] (proj_a.buildGraph.example.com, 1.0.0@e479b438) 93 | [Warn] (proj_a.buildGraph.example.com) [x] (proj_a.buildGraph.example.com, 1.1.0@fb3804df^) 94 | [Warn] (proj_b.buildGraph.example.com) [ ] (proj_b.buildGraph.example.com, 1.0.0@af427510) 95 | [Warn] (proj_b.buildGraph.example.com) [x] (proj_b.buildGraph.example.com, 1.1.0@ee875bae^) 96 | [Warn] (proj_c.buildGraph.example.com) [x] (proj_c.buildGraph.example.com, 1.2.0@9331e14f^) 97 | [Warn] (proj_d.buildGraph.example.com) [x] (proj_d.buildGraph.example.com, 1.0.0@0dec9c97) 98 | [Warn] (proj_d.buildGraph.example.com) [!] (HasBrokenDep; pkg: proj_d.buildGraph.example.com, 2.0.0@dd98f775^) 99 | [Warn] (Resolved) end of selection 100 | """ 101 | testSemVer2("semproject1", semVerExpectedResult) 102 | 103 | test "semproject2": 104 | withDir "tests/ws_semproject2": 105 | removeDirs("semproject2") 106 | removeDirs("deps") 107 | setupGraphNoGitTags() 108 | let semVerExpectedResultNoGitTags = dedent""" 109 | [Warn] (Resolved) selected: 110 | [Warn] (proj_a.buildGraphNoGitTags.example.com) [ ] (proj_a.buildGraphNoGitTags.example.com, 1.0.0@88d1801b) 111 | [Warn] (proj_a.buildGraphNoGitTags.example.com) [ ] (proj_a.buildGraphNoGitTags.example.com, 1.0.0@6a1cc178) 112 | [Warn] (proj_a.buildGraphNoGitTags.example.com) [x] (proj_a.buildGraphNoGitTags.example.com, 1.1.0@61eacba5^) 113 | [Warn] (proj_b.buildGraphNoGitTags.example.com) [ ] (proj_b.buildGraphNoGitTags.example.com, 1.0.0@289ae9ee) 114 | [Warn] (proj_b.buildGraphNoGitTags.example.com) [ ] (proj_b.buildGraphNoGitTags.example.com, 1.0.0@bbb208a9) 115 | [Warn] (proj_b.buildGraphNoGitTags.example.com) [x] (proj_b.buildGraphNoGitTags.example.com, 1.1.0@c70824d8^) 116 | [Warn] (proj_c.buildGraphNoGitTags.example.com) [ ] (proj_c.buildGraphNoGitTags.example.com, 1.0.0@8756fa45) 117 | [Warn] (proj_c.buildGraphNoGitTags.example.com) [x] (proj_c.buildGraphNoGitTags.example.com, 1.2.0@d6c04d67^) 118 | [Warn] (proj_d.buildGraphNoGitTags.example.com) [x] (proj_d.buildGraphNoGitTags.example.com, 1.0.0@0bd0e77a) 119 | [Warn] (proj_d.buildGraphNoGitTags.example.com) [!] (HasBrokenDep; pkg: proj_d.buildGraphNoGitTags.example.com, 2.0.0@7ee36fec^) 120 | [Warn] (Resolved) end of selection 121 | """ 122 | 123 | testSemVer2("semproject2", semVerExpectedResultNoGitTags) 124 | 125 | test "minproject1": 126 | withDir "tests/ws_minproject1": 127 | removeDirs("deps") 128 | setupGraph() 129 | let minVerExpectedResult = dedent""" 130 | [Warn] (Resolved) selected: 131 | [Warn] (proj_a.buildGraph.example.com) [x] (proj_a.buildGraph.example.com, 1.0.0@e479b438) 132 | [Warn] (proj_a.buildGraph.example.com) [ ] (proj_a.buildGraph.example.com, 1.1.0@fb3804df^) 133 | [Warn] (proj_b.buildGraph.example.com) [x] (proj_b.buildGraph.example.com, 1.0.0@af427510) 134 | [Warn] (proj_b.buildGraph.example.com) [ ] (proj_b.buildGraph.example.com, 1.1.0@ee875bae^) 135 | [Warn] (proj_c.buildGraph.example.com) [x] (proj_c.buildGraph.example.com, 1.2.0@9331e14f^) 136 | [Warn] (proj_d.buildGraph.example.com) [x] (proj_d.buildGraph.example.com, 1.0.0@0dec9c97) 137 | [Warn] (proj_d.buildGraph.example.com) [!] (HasBrokenDep; pkg: proj_d.buildGraph.example.com, 2.0.0@dd98f775^) 138 | [Warn] (Resolved) end of selection 139 | """ 140 | testMinVer("minproject", minVerExpectedResult) 141 | -------------------------------------------------------------------------------- /tests/tgitops.nim: -------------------------------------------------------------------------------- 1 | import unittest 2 | import std/[os, files, dirs, paths, osproc, strutils, uri] 3 | import basic/[reporters, osutils, versions, context] 4 | 5 | import basic/gitops 6 | import testerutils 7 | 8 | ensureGitHttpServer() 9 | 10 | suite "Git Operations Tests": 11 | var 12 | c: AtlasContext 13 | reporter: Reporter 14 | testDir = Path "tests/test_repo" 15 | 16 | setup: 17 | # Create a fresh test directory 18 | removeDir(testDir) 19 | createDir(testDir) 20 | c = AtlasContext(flags: {DumbProxy}) 21 | reporter = Reporter() 22 | 23 | teardown: 24 | # Clean up test directory 25 | removeDir(testDir) 26 | 27 | test "isGitDir detection": 28 | check(not isGitDir(testDir)) 29 | discard execCmd("git init " & $testDir) 30 | check(isGitDir(testDir)) 31 | 32 | test "sameVersionAs comparisons": 33 | check(sameVersionAs("v1.0.0", "1.0.0")) 34 | check(sameVersionAs("release-1.2.3", "1.2.3")) 35 | check(not sameVersionAs("v1.0.1", "1.0.0")) 36 | check(not sameVersionAs("v10.0.0", "1.0.0")) 37 | 38 | test "extractVersion from strings": 39 | check(extractVersion("v1.0.0") == "1.0.0") 40 | check(extractVersion("release-2.3.4") == "2.3.4") 41 | check(extractVersion("prefix_5.0.1_suffix") == "5.0.1_suffix") 42 | 43 | test "Git command execution": 44 | # Initialize test repo 45 | withDir testDir: 46 | discard execCmd("git init") 47 | check(isGitDir(Path ".")) 48 | 49 | # Test git diff command 50 | let (diffOutput, diffStatus) = exec(GitDiff, Path ".", []) 51 | check(diffStatus.int == 0) 52 | check(diffOutput.len == 0) 53 | 54 | test "Version to commit resolution": 55 | withDir testDir: 56 | discard execCmd("git init") 57 | # Create and tag a test commit 58 | writeFile("test.txt", "test content") 59 | discard execCmd("git add test.txt") 60 | discard execCmd("git commit -m \"test commit\"") 61 | discard execCmd("git tag v1.0.0") 62 | 63 | var err = false 64 | let commit = versionToCommit(Path ".", MinVer, parseVersionInterval("1.0.0", 0, err)) 65 | check(not commit.isEmpty) 66 | 67 | test "Git clone functionality": 68 | let testUrl = parseUri "http://localhost:4242/buildGraph/proj_a.git" 69 | let res = clone(testUrl, testDir) 70 | # Note: This will fail if gitHttpServer isn't running 71 | check(res[0] == Ok) # Expected to fail since URL is fake 72 | 73 | test "incrementTag behavior": 74 | check(incrementTag("test", "v1.0.0", 2) == "v1.0.1") 75 | check(incrementTag("test", "v2.3.4", 1) == "v2.4.4") 76 | check(incrementTag("test", "1.0.0", 0) == "2.0.0") 77 | 78 | # test "needsCommitLookup detection": 79 | # check(needsCommitLookup("1.0.0")) 80 | # check(needsCommitLookup("v1.2.3")) 81 | # check(not needsCommitLookup("abc123")) 82 | # check(not needsCommitLookup("1234567890abcdef1234567890abcdef12345678")) 83 | 84 | test "isShortCommitHash validation": 85 | check(isShortCommitHash("abcd")) 86 | check(isShortCommitHash("1234567")) 87 | check(not isShortCommitHash("abc")) 88 | check(not isShortCommitHash("1234567890abcdef1234567890abcdef12345678")) 89 | 90 | test "isGitDir detection": 91 | check(not isGitDir(testDir)) 92 | discard execCmd("git init " & $testDir) 93 | check(isGitDir(testDir)) 94 | 95 | test "sameVersionAs comparisons": 96 | check(sameVersionAs("v1.0.0", "1.0.0")) 97 | check(sameVersionAs("release-1.2.3", "1.2.3")) 98 | check(not sameVersionAs("v1.0.1", "1.0.0")) 99 | check(not sameVersionAs("v10.0.0", "1.0.0")) 100 | 101 | test "incrementLastTag behavior": 102 | withDir testDir: 103 | discard execCmd("git init") 104 | # Create initial commit and tag 105 | writeFile("test.txt", "initial content") 106 | discard execCmd("git add test.txt") 107 | discard execCmd("git commit -m \"initial commit\"") 108 | discard execCmd("git tag v1.0.0") 109 | writeFile("test.txt", "more content") 110 | discard execCmd("git commit -am \"second commit\"") 111 | 112 | # Test incrementing different version fields 113 | check(incrementLastTag(Path ".", 0) == "v2.0.0") 114 | check(incrementLastTag(Path ".", 1) == "v1.1.0") 115 | check(incrementLastTag(Path ".", 2) == "v1.0.1") 116 | 117 | test "incrementLastTag behavior no tags": 118 | withDir testDir: 119 | # Test with no tags 120 | discard execCmd("git init ") 121 | writeFile("test.txt", "initial content") 122 | discard execCmd("git add test.txt") 123 | discard execCmd("git commit -m \"initial commit\"") 124 | check(incrementLastTag(Path ".", 0) == "v0.0.1") 125 | 126 | test "isOutdated detection": 127 | withDir testDir: 128 | discard execCmd("git init") 129 | # Create initial commit and tag 130 | writeFile("test.txt", "initial content") 131 | discard execCmd("git add test.txt") 132 | discard execCmd("git commit -m \"initial commit\"") 133 | discard execCmd("git tag v1.0.0") 134 | 135 | # Create new commit without tag 136 | writeFile("test.txt", "updated content") 137 | discard execCmd("git add test.txt") 138 | discard execCmd("git commit -m \"update commit\"") 139 | 140 | # Test if repo is outdated 141 | let outdated = isOutdated(Path ".") 142 | # Note: This might fail in isolated test environments 143 | # We're mainly testing the function structure 144 | check(not outdated) # Expected to be false in test environment 145 | 146 | test "getRemoteUrl functionality": 147 | withDir testDir: 148 | discard execCmd("git init") 149 | let testUrl = "https://github.com/test/repo.git" 150 | discard execCmd("git remote add origin " & testUrl) 151 | 152 | # Test getting remote URL 153 | let url = getRemoteUrl(Path ".") 154 | check(url == testUrl) 155 | 156 | # Test getting remote URL from specific directory 157 | # let dirUrl = getRemoteUrl(c, testDir) 158 | # check(dirUrl == testUrl) 159 | 160 | test "checkGitDiffStatus behavior": 161 | withDir testDir: 162 | discard execCmd("git init") 163 | 164 | # Test clean state 165 | let cleanStatus = checkGitDiffStatus(Path ".") 166 | check(cleanStatus == "") 167 | 168 | # Test with uncommitted changes 169 | writeFile("test.txt", "some content") 170 | discard execCmd("git add test.txt") 171 | writeFile("test.txt", "modified content") 172 | let dirtyStatus = checkGitDiffStatus(Path ".") 173 | check(dirtyStatus == "'git diff' not empty") 174 | 175 | # Test with committed changes 176 | discard execCmd("git add test.txt") 177 | discard execCmd("git commit -m \"test commit\"") 178 | let committedStatus = checkGitDiffStatus(Path ".") 179 | check(committedStatus == "") 180 | 181 | test "gitDescribeRefTag functionality": 182 | withDir testDir: 183 | discard execCmd("git init") 184 | 185 | # Create and tag a commit 186 | writeFile("test.txt", "initial content") 187 | discard execCmd("git add test.txt") 188 | discard execCmd("git commit -m \"initial commit\"") 189 | let initialCommit = execProcess("git rev-parse HEAD").strip() 190 | discard execCmd("git tag v1.0.0") 191 | 192 | # Test describing the tagged commit 193 | let tagDescription = gitDescribeRefTag(Path ".", initialCommit) 194 | check(tagDescription == "v1.0.0") 195 | 196 | # Test describing an untagged commit 197 | writeFile("test.txt", "updated content") 198 | discard execCmd("git add test.txt") 199 | discard execCmd("git commit -m \"update commit\"") 200 | let newCommit = execProcess("git rev-parse HEAD").strip() 201 | let untaggedDescription = gitDescribeRefTag(Path ".", newCommit) 202 | check(untaggedDescription.startsWith("v1.0.0-1-")) 203 | 204 | test "collectTaggedVersions functionality": 205 | withDir testDir: 206 | discard execCmd("git init") 207 | 208 | # Create initial commit and tag 209 | writeFile("test.txt", "initial content") 210 | discard execCmd("git add test.txt") 211 | discard execCmd("git commit -m \"initial commit\"") 212 | discard execCmd("git tag v1.0.0") 213 | 214 | # Add more commits and tags 215 | writeFile("test.txt", "second version") 216 | discard execCmd("git add test.txt") 217 | discard execCmd("git commit -m \"second commit\"") 218 | discard execCmd("git tag v1.1.0") 219 | 220 | writeFile("test.txt", "third version") 221 | discard execCmd("git add test.txt") 222 | discard execCmd("git commit -m \"third commit\"") 223 | discard execCmd("git tag v2.0.0") 224 | 225 | # Test collecting all tagged versions 226 | let versions = collectTaggedVersions(Path ".") 227 | check(versions.len == 3) 228 | check($versions[0].v == "2.0.0") 229 | check($versions[1].v == "1.1.0") 230 | check($versions[2].v == "1.0.0") 231 | 232 | # Verify commit hashes are present 233 | for v in versions: 234 | check(v.c.h.len == 40) # Full SHA-1 hash length 235 | -------------------------------------------------------------------------------- /tests/tnimbleparser.nim: -------------------------------------------------------------------------------- 1 | import std/[unittest, os, algorithm, strutils, importutils, terminal] 2 | import basic/[context, pkgurls, deptypes, nimblecontext, compiledpatterns, osutils, versions] 3 | import basic/nimbleparser 4 | import basic/parse_requires 5 | 6 | import testerutils 7 | 8 | proc doesContain(res: NimbleFileInfo, name: string): bool = 9 | for req in res.requires: 10 | if req.contains(name): 11 | result = true 12 | 13 | suite "nimbleparser": 14 | test "parse nimble file when not defined(windows)": 15 | let nimbleFile = Path("tests" / "test_data" / "jester.nimble") 16 | var res = extractRequiresInfo(nimbleFile) 17 | echo "Nimble release: ", repr res 18 | when defined(windows): 19 | check not doesContain(res, "httpbeast") 20 | else: 21 | check doesContain(res, "httpbeast") 22 | 23 | test "parse nimble file when defined(windows)": 24 | let nimbleFile = Path("tests" / "test_data" / "jester_inverted.nimble") 25 | var res = extractRequiresInfo(nimbleFile) 26 | echo "Nimble release: ", repr res 27 | when defined(windows): 28 | check not doesContain(res, "httpbeast") 29 | else: 30 | check doesContain(res, "httpbeast") 31 | 32 | test "parse nimble file when macos or linux": 33 | let nimbleFile = Path("tests" / "test_data" / "jester_combined.nimble") 34 | var res = extractRequiresInfo(nimbleFile) 35 | echo "Nimble release: ", repr res 36 | when defined(macosx) or defined(linux): 37 | check doesContain(res, "httpbeast") 38 | else: 39 | check not doesContain(res, "httpbeast") 40 | 41 | test "parse nimble file with features": 42 | setAtlasVerbosity(Trace) 43 | let nimbleFile = Path("tests" / "test_data" / "jester_feature.nimble") 44 | var res = extractRequiresInfo(nimbleFile) 45 | echo "Nimble release: ", $res 46 | check res.requires.len == 1 47 | check res.features.len == 3 48 | check res.features.hasKey("useHttpbeast") 49 | check res.features["useHttpbeast"].len == 1 50 | check res.features["useHttpbeast"][0] == "httpbeast >= 0.4.0" 51 | check res.features.hasKey("useAsyncTools") 52 | check res.features["useAsyncTools"].len == 1 53 | check res.features["useAsyncTools"][0] == "asynctools >= 0.1.0" 54 | check res.features.hasKey("useOldAsyncTools") 55 | check res.features["useOldAsyncTools"].len == 1 56 | check res.features["useOldAsyncTools"][0] == "asynctools >= 0.1.0" 57 | 58 | test "parse nimble file with when statements": 59 | let nimbleFile = Path("tests" / "test_data" / "jester_boolean.nimble") 60 | var res = extractRequiresInfo(nimbleFile) 61 | echo "Nimble release: ", $res 62 | 63 | # Check basic package info is parsed correctly 64 | check res.version == "0.6.0" 65 | 66 | # Should always have the base requirement 67 | check doesContain(res, "nim >= 1.0.0") 68 | 69 | # Count how many httpbeast requirements we expect based on platform 70 | var expectedHttpbeastCount = 0 71 | 72 | # when defined(linux): -> only true on Linux 73 | when defined(linux): 74 | expectedHttpbeastCount += 1 75 | 76 | # when defined(linux) or defined(macosx): -> true on Linux or macOS 77 | when defined(linux) or defined(macosx): 78 | expectedHttpbeastCount += 1 79 | 80 | # when not defined(linux) or defined(macosx): -> true when NOT Linux OR when macOS 81 | # This is true on macOS, Windows, and other non-Linux platforms 82 | when not defined(linux) or defined(macosx): 83 | expectedHttpbeastCount += 1 84 | 85 | # when not (defined(linux) or defined(macosx)): -> true when neither Linux nor macOS 86 | when not (defined(linux) or defined(macosx)): 87 | expectedHttpbeastCount += 1 88 | 89 | # when defined(windows) and (defined(linux) or defined(macosx)): -> impossible, always false 90 | when defined(windows) and (defined(linux) or defined(macosx)): 91 | expectedHttpbeastCount += 1 92 | 93 | # Count actual httpbeast requirements in the result 94 | var actualHttpbeastCount = 0 95 | for req in res.requires: 96 | if req.contains("httpbeast"): 97 | actualHttpbeastCount += 1 98 | 99 | echo "Expected httpbeast count: ", expectedHttpbeastCount 100 | echo "Actual httpbeast count: ", actualHttpbeastCount 101 | check actualHttpbeastCount == expectedHttpbeastCount 102 | 103 | # Verify no errors occurred during parsing 104 | check not res.hasErrors 105 | 106 | test "parse nimble file with when statements runtime defines": 107 | let nimbleFile = Path("tests" / "test_data" / "jester_boolean.nimble") 108 | 109 | setBasicDefines("linux", true) 110 | setBasicDefines("macosx", true) 111 | setBasicDefines("windows", true) 112 | setBasicDefines("posix", true) 113 | setBasicDefines("freebsd", false) 114 | setBasicDefines("openbsd", false) 115 | setBasicDefines("netbsd", false) 116 | 117 | var res = extractRequiresInfo(nimbleFile) 118 | echo "Nimble release: ", $res 119 | 120 | # Check basic package info is parsed correctly 121 | check res.version == "0.6.0" 122 | 123 | # Should always have the base requirement 124 | check doesContain(res, "nim >= 1.0.0") 125 | 126 | # Count how many httpbeast requirements we expect based on platform 127 | var expectedHttpbeastCount = 4 128 | 129 | var actualHttpbeastCount = 0 130 | for req in res.requires: 131 | echo "got req: ", req 132 | if req.contains("httpbeast"): 133 | actualHttpbeastCount += 1 134 | 135 | echo "Expected httpbeast count: ", expectedHttpbeastCount 136 | echo "Actual httpbeast count: ", actualHttpbeastCount 137 | check actualHttpbeastCount == expectedHttpbeastCount 138 | 139 | -------------------------------------------------------------------------------- /tests/tserde.nim: -------------------------------------------------------------------------------- 1 | import std/[unittest, json, jsonutils] 2 | import basic/[context, sattypes, pkgurls, deptypes, nimblecontext, depgraphtypes] 3 | import basic/[deptypesjson, versions] 4 | 5 | proc p(s: string): VersionInterval = 6 | var err = false 7 | result = parseVersionInterval(s, 0, err) 8 | # assert not err 9 | 10 | suite "json serde": 11 | setup: 12 | var nc = createUnfilledNimbleContext() 13 | nc.put("foobar", toPkgUriRaw(parseUri "https://github.com/nimble-test/foobar.git")) 14 | nc.put("proj_a", toPkgUriRaw(parseUri "https://example.com/buildGraph/proj_a")) 15 | 16 | test "pkg url": 17 | let upkg = nc.createUrl("foobar") 18 | let jn = toJson(upkg) 19 | var upkg2: PkgUrl 20 | upkg2.fromJson(jn) 21 | check upkg == upkg2 22 | echo "upkg2: ", $(upkg2) 23 | 24 | let url2 = nc.createUrl("https://github.com/nimble-test/foobar") 25 | check url2.projectName() == "foobar" 26 | 27 | 28 | test "pkg url, version interval": 29 | let upkg = nc.createUrl("foobar") 30 | let jn = toJson((upkg, p"1.0.0")) 31 | var upkg2: (PkgUrl, VersionInterval) 32 | upkg2.fromJson(jn) 33 | check upkg2[0] == upkg 34 | 35 | test "json serde ordered table": 36 | var table: OrderedTable[PkgUrl, Package] 37 | let url = nc.createUrl("foobar") 38 | var pkg = Package(url: url) 39 | table[url] = pkg 40 | let jn = toJson(table) 41 | var table2: OrderedTable[PkgUrl, Package] 42 | table2.fromJson(jn) 43 | # note this will fail because the url doesn't use nimble context 44 | # check table == table2 45 | 46 | test "json dep graph": 47 | var graph = DepGraph() 48 | let url = nc.createUrl("foobar") 49 | var pkg = Package(url: url) 50 | graph.root = pkg 51 | graph.pkgs[url] = pkg 52 | let url2 = nc.createUrl("proj_a") 53 | var pkg2 = Package(url: url2) 54 | graph.pkgs[url2] = pkg2 55 | 56 | let jn = toJsonGraph(graph) 57 | var graph2 = loadJson(nc, jn) 58 | 59 | echo "root: ", graph.root.repr 60 | echo "root2: ", graph2.root.repr 61 | 62 | echo "root2.url: ", $(graph2.root.url), " project name: ", graph2.root.url.projectName() 63 | 64 | check graph.root.hash() == graph2.root.hash() 65 | 66 | check graph.pkgs[url].hash() == graph2.pkgs[url].hash() 67 | check graph.pkgs[url2].hash() == graph2.pkgs[url2].hash() 68 | 69 | test "json serde nimble release": 70 | let release = NimbleRelease(version: Version"1.0.0", requirements: @[(nc.createUrl("foobar"), p"1.0.0")]) 71 | let jnRelease = toJson(release) 72 | echo "jnRelease: ", pretty(jnRelease) 73 | var release2: NimbleRelease 74 | release2.fromJson(jnRelease) 75 | check release == release2 76 | 77 | test "json serde version interval": 78 | 79 | let interval = p"1.0.0" 80 | let jn = toJson(interval) 81 | var interval2 = VersionInterval() 82 | interval2.fromJson(jn) 83 | check interval == interval2 84 | 85 | let query = p">= 1.2 & < 1.4" 86 | let jn2 = toJson(query) 87 | var query2 = VersionInterval() 88 | query2.fromJson(jn2) 89 | check query == query2 90 | 91 | test "var ids": 92 | let var1 = VarId(1) 93 | let jn = toJson(var1) 94 | var var2 = VarId(0) 95 | var2.fromJson(jn) 96 | check var1.int == var2.int 97 | 98 | test "path": 99 | let path1 = Path("test.nim") 100 | let jn = toJson(path1) 101 | var path2: Path 102 | path2.fromJson(jn) 103 | check path1 == path2 104 | 105 | test "test version tag and commit hash str": 106 | let c1 = initCommitHash("24870f48c40da2146ce12ff1e675e6e7b9748355", FromNone) 107 | let v1 = VersionTag(v: Version"#head", c: c1) 108 | 109 | check $c1 == "24870f48c40da2146ce12ff1e675e6e7b9748355" 110 | check $v1 == "#head@24870f48" 111 | 112 | let v2 = toVersionTag("#head@24870f48c40da2146ce12ff1e675e6e7b9748355") 113 | check $v2 == "#head@24870f48" 114 | check repr(v2) == "#head@24870f48c40da2146ce12ff1e675e6e7b9748355" 115 | 116 | let v3 = toVersionTag("#head@-") 117 | check v3.v.string == "#head" 118 | check v3.c.h == "" 119 | check $v3 == "#head@-" 120 | 121 | let v4 = VersionTag(v: Version"#head", c: initCommitHash("", FromGitTag)) 122 | check v4 == v3 123 | 124 | let jn = toJson(v1) 125 | var v5 = VersionTag() 126 | v5.fromJson(jn) 127 | check v5 == v1 128 | echo "v5: ", repr(v5) 129 | 130 | let jn2 = toJson(c1) 131 | var c2 = CommitHash() 132 | c2.fromJson(jn2) 133 | check c2 == c1 134 | echo "c2: ", repr(c2) 135 | 136 | let jn3 = toJson(v3) 137 | var v6 = VersionTag() 138 | v6.fromJson(jn3) 139 | check v6 == v3 140 | echo "v6: ", repr(v6) 141 | 142 | let jn4 = toJson(v4) 143 | var v7 = VersionTag() 144 | v7.fromJson(jn4) 145 | check v7 == v4 146 | echo "v7: ", repr(v7) 147 | 148 | test "test empty version tag": 149 | let v8 = VersionTag() 150 | echo "v8: ", repr(v8) 151 | let jn = toJson(v8) 152 | 153 | var v9 = VersionTag() 154 | v9.fromJson(jn) 155 | check v9 == v8 156 | echo "v9: ", repr(v9) 157 | 158 | 159 | -------------------------------------------------------------------------------- /tests/ttnimbleparser.nims: -------------------------------------------------------------------------------- 1 | --noNimblePath 2 | --path:"../src" 3 | --path:"$nim" 4 | --d:atlasStandAlone 5 | --d:atlasUnitTests -------------------------------------------------------------------------------- /tests/ws_basic/atlas.config: -------------------------------------------------------------------------------- 1 | { 2 | "deps": "", 3 | "overrides": "", 4 | "plugins": "", 5 | "resolver": "SemVer", 6 | "graph": null 7 | } -------------------------------------------------------------------------------- /tests/ws_basic/deps/foobar.nimble-link: -------------------------------------------------------------------------------- 1 | ../remote-deps/foobar/foobar.nimble 2 | ../remote-deps/foobar/ -------------------------------------------------------------------------------- /tests/ws_basic/packages/packages.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "bytes2human", 4 | "url": "https://github.com/juancarlospaco/nim-bytes2human", 5 | "method": "git", 6 | "tags": [ 7 | "bytes", 8 | "human", 9 | "minimalism", 10 | "size" 11 | ], 12 | "description": "Convert bytes to kilobytes, megabytes, gigabytes, etc.", 13 | "license": "LGPLv3", 14 | "web": "https://github.com/juancarlospaco/nim-bytes2human" 15 | }, 16 | { 17 | "name": "npeg", 18 | "url": "https://github.com/zevv/npeg", 19 | "method": "git", 20 | "tags": [ 21 | "PEG", 22 | "parser", 23 | "parsing", 24 | "regexp", 25 | "regular", 26 | "grammar", 27 | "lexer", 28 | "lexing", 29 | "pattern", 30 | "matching" 31 | ], 32 | "description": "PEG (Parsing Expression Grammars) string matching library for Nim", 33 | "license": "MIT", 34 | "web": "https://github.com/zevv/npeg" 35 | }, 36 | { 37 | "name": "sync", 38 | "url": "https://github.com/planetis-m/sync", 39 | "method": "git", 40 | "tags": [ 41 | "synchronization", 42 | "multithreading", 43 | "parallelism", 44 | "threads" 45 | ], 46 | "description": "Useful synchronization primitives", 47 | "license": "MIT", 48 | "web": "https://github.com/planetis-m/sync" 49 | } 50 | ] -------------------------------------------------------------------------------- /tests/ws_basic/remote-deps/foobar/foobar.nimble: -------------------------------------------------------------------------------- 1 | version = "0.1.0" 2 | srcDir = "." -------------------------------------------------------------------------------- /tests/ws_conflict/atlas.config: -------------------------------------------------------------------------------- 1 | { 2 | "deps": "", 3 | "overrides": "url.rules", 4 | "plugins": "", 5 | "resolver": "MinVer", 6 | "graph": null 7 | } -------------------------------------------------------------------------------- /tests/ws_conflict/expected/deps.dot: -------------------------------------------------------------------------------- 1 | digraph deps { 2 | "file://./source/apkg/#head" [label=""]; 3 | "file://./source/bpkg/1.0" [label=""]; 4 | "file://./source/cpkg/1.0" [label="unused"]; 5 | "file://./source/cpkg/2.0" [label=""]; 6 | "file://./source/dpkg/1.0" [label=""]; 7 | "file://./source/apkg/#head" -> "file://./source/bpkg/1.0"; 8 | "file://./source/apkg/#head" -> "file://./source/cpkg/1.0"; 9 | "file://./source/bpkg/1.0" -> "file://./source/cpkg/2.0"; 10 | "file://./source/cpkg/2.0" -> "file://./source/dpkg/1.0"; 11 | } 12 | -------------------------------------------------------------------------------- /tests/ws_conflict/expected/myproject.nimble: -------------------------------------------------------------------------------- 1 | requires "https://github.com/apkg" 2 | -------------------------------------------------------------------------------- /tests/ws_conflict/expected/nim.cfg: -------------------------------------------------------------------------------- 1 | ############# begin Atlas config section ########## 2 | --noNimblePath 3 | --path:"../apkg" 4 | --path:"../bpkg" 5 | --path:"../cpkg" 6 | --path:"../dpkg" 7 | ############# end Atlas config section ########## 8 | -------------------------------------------------------------------------------- /tests/ws_conflict/source/apkg/apkg.nimble: -------------------------------------------------------------------------------- 1 | # require first b and then c 2 | 3 | requires "https://github.com/bpkg >= 1.0" 4 | requires "https://github.com/cpkg >= 1.0" 5 | -------------------------------------------------------------------------------- /tests/ws_conflict/source/bpkg@1.0/bpkg.nimble: -------------------------------------------------------------------------------- 1 | requires "https://github.com/cpkg >= 2.0" 2 | -------------------------------------------------------------------------------- /tests/ws_conflict/source/cpkg@1.0/cpkg.nimble: -------------------------------------------------------------------------------- 1 | # No dependency here! 2 | -------------------------------------------------------------------------------- /tests/ws_conflict/source/cpkg@2.0/cpkg.nimble: -------------------------------------------------------------------------------- 1 | requires "https://github.com/dpkg >= 1.0" 2 | -------------------------------------------------------------------------------- /tests/ws_conflict/source/dpkg/dpkg.nimble: -------------------------------------------------------------------------------- 1 | # empty for now 2 | -------------------------------------------------------------------------------- /tests/ws_conflict/url.rules: -------------------------------------------------------------------------------- 1 | https://github.com/$+ -> file://./source/$# 2 | -------------------------------------------------------------------------------- /tests/ws_features/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nim-lang/atlas/40db39a7d7ef5f29a37d0ac226b0a1429a469ad1/tests/ws_features/.gitkeep -------------------------------------------------------------------------------- /tests/ws_features/atlas.config: -------------------------------------------------------------------------------- 1 | { 2 | "deps": "", 3 | "nameOverrides": { 4 | "proj$+": "https://example.com/buildGraph/proj$#" 5 | }, 6 | "urlOverrides": {}, 7 | "plugins": "", 8 | "resolver": "SemVer", 9 | "graph": null 10 | } -------------------------------------------------------------------------------- /tests/ws_features/ws_features.nimble: -------------------------------------------------------------------------------- 1 | requires "proj_a[testing]" 2 | -------------------------------------------------------------------------------- /tests/ws_features_global/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nim-lang/atlas/40db39a7d7ef5f29a37d0ac226b0a1429a469ad1/tests/ws_features_global/.gitkeep -------------------------------------------------------------------------------- /tests/ws_features_global/atlas.config: -------------------------------------------------------------------------------- 1 | { 2 | "deps": "", 3 | "nameOverrides": { 4 | "proj$+": "https://example.com/buildGraph/proj$#" 5 | }, 6 | "urlOverrides": {}, 7 | "plugins": "", 8 | "resolver": "SemVer", 9 | "graph": null 10 | } -------------------------------------------------------------------------------- /tests/ws_features_global/ws_features_global.nimble: -------------------------------------------------------------------------------- 1 | requires "proj_a" 2 | -------------------------------------------------------------------------------- /tests/ws_integration/.gitignore: -------------------------------------------------------------------------------- 1 | ws_integration.nimble 2 | atlas.lock 3 | -------------------------------------------------------------------------------- /tests/ws_integration/atlas.config: -------------------------------------------------------------------------------- 1 | { 2 | "deps": "deps", 3 | "nameOverrides": {}, 4 | "urlOverrides": {}, 5 | "pkgOverrides": { 6 | "asynctools": "https://github.com/timotheecour/asynctools" 7 | }, 8 | "plugins": "", 9 | "resolver": "MaxVer", 10 | "graph": null 11 | } -------------------------------------------------------------------------------- /tests/ws_integration/expected/nim.cfg: -------------------------------------------------------------------------------- 1 | ############# begin Atlas config section ########## 2 | --noNimblePath 3 | --path:"deps/nitter.zedeus.github.com/src" 4 | --path:"deps/supersnappy/src" 5 | --path:"deps/packedjson" 6 | --path:"deps/redpool/src" 7 | --path:"deps/karax" 8 | --path:"deps/markdown/src" 9 | --path:"deps/jsony/src" 10 | --path:"deps/oauth/src" 11 | --path:"deps/nimcrypto" 12 | --path:"deps/httpbeast/src" 13 | --path:"deps/redis.zedeus.github.com/src" 14 | --path:"deps/jester" 15 | --path:"deps/zippy/src" 16 | --path:"deps/flatty/src" 17 | --path:"deps/sass/src" 18 | --path:"deps/ws/src" 19 | --path:"deps/asynctools.timotheecour.github.com" 20 | --path:"deps/sha1" 21 | --path:"deps/dotenv/src" 22 | ############# end Atlas config section ########## 23 | -------------------------------------------------------------------------------- /tests/ws_integration/nimble.paths: -------------------------------------------------------------------------------- 1 | --noNimblePath 2 | --path:"/Users/elcritch/.nimble/pkgs2/supersnappy-2.1.3-a197de45773703f7134301043e416fb5025c614d" 3 | --path:"/Users/elcritch/.nimble/pkgs2/zippy-0.10.7-8ec66d2af90ca93f96c0388ae933f03a60ff67d1" 4 | --path:"/Users/elcritch/.nimble/pkgs2/jester-0.5.0-afb591b0a6cd39f4d5f2195bffb7095640e3da97" 5 | --path:"/Users/elcritch/.nimble/pkgs2/redis-0.3.0-9b1a45983890db1f1775ecddc51925000d183c57" 6 | --path:"/Users/elcritch/.nimble/pkgs2/sass-0.3.0-21e4dcb1ab3d32d343df1d8293affa9ea9c92f70" 7 | --path:"/Users/elcritch/.nimble/pkgs2/nim-2.0.8-46333e8f4bda41dd6d3852a3f5fa4975b96b66a2" 8 | --path:"/Users/elcritch/.nimble/pkgs2/dotenv-2.0.2-1e70fc63c286ca3da7592d61dbe501fcea35bc72" 9 | --path:"/Users/elcritch/.nimble/pkgs2/nimcrypto-0.6.0-be9a4654dd5839b97f39d5060d98f18ba868623c" 10 | --path:"/Users/elcritch/.nimble/pkgs2/packedjson-0.2.2-8aec1ecc233e2ac4d04f0a3dd02d03c73d7b4665" 11 | --path:"/Users/elcritch/.nimble/pkgs2/karax-1.3.0-481742b3d0c0e3c92174b1bdd08a4a94853250af" 12 | --path:"/Users/elcritch/.nimble/pkgs2/sha1-1.1-2610d27cf248adf98fd9b86e906eb87781ba9d8c" 13 | --path:"/Users/elcritch/.nimble/pkgs2/oauth-0.10-55b2ff9c5051f3bc8712f39b942556c493b0a9a5" 14 | --path:"/Users/elcritch/.nimble/pkgs2/ws-0.5.0-ae4daf4ae302d0431f3c2d385ae9d2fe767a3246" 15 | --path:"/Users/elcritch/.nimble/pkgs2/asynctools-0.1.1-54314dceabb06b20908ecb0f2c007e9ff3aaa054" 16 | --path:"/Users/elcritch/.nimble/pkgs2/markdown-0.8.6-53b43fa57e6aa106e634ae97b793f1c6e7da7ce6" 17 | --path:"/Users/elcritch/.nimble/pkgs2/asynctools-0.1.1-5e7df4178ee5c148532ff8c44a22015d576624d8" 18 | --path:"/Users/elcritch/.nimble/pkgs2/jsony-1.1.5-143f6938d399f92f5bf6c7dcf4e589f2b28634db" 19 | --path:"/Users/elcritch/.nimble/pkgs2/httpbeast-0.4.1-b23e57a401057dcb9b7fae1fb8279a6a2ce1d0b8" 20 | --path:"/Users/elcritch/.nimble/pkgs2/redpool-0.1.0-3d2665fc592c25cbe5d253957bc16e9666227c3d" 21 | --path:"/Users/elcritch/.nimble/pkgs2/flatty-0.3.4-48109ac5d2f65f9d1a61f5077ba0eb61d104a90a" -------------------------------------------------------------------------------- /tests/ws_integration/ws_integration.nimble: -------------------------------------------------------------------------------- 1 | requires "https://github.com/zedeus/nitter#92cd6abcf6d9935bc0d7f013acbfbfd8ddd896ba" 2 | 3 | -------------------------------------------------------------------------------- /tests/ws_link_integration/deps/atlas.config: -------------------------------------------------------------------------------- 1 | { 2 | "deps": "deps", 3 | "nameOverrides": { 4 | "ws_link_semver": "link:///Volumes/projects/nims/atlas/tests/ws_link_semver/ws_link_semver.nimble", 5 | "proj_b": "https://example.com/buildGraph/proj_b", 6 | "proj_d": "https://example.com/buildGraph/proj_d", 7 | "proj_c": "https://example.com/buildGraph/proj_c", 8 | "proj_a": "https://example.com/buildGraph/proj_a" 9 | }, 10 | "urlOverrides": {}, 11 | "pkgOverrides": {}, 12 | "plugins": "", 13 | "resolver": "SemVer", 14 | "graph": null 15 | } -------------------------------------------------------------------------------- /tests/ws_link_integration/ws_link_integration.nimble: -------------------------------------------------------------------------------- 1 | requires "ws_link_semver" 2 | 3 | -------------------------------------------------------------------------------- /tests/ws_link_semver/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nim-lang/atlas/40db39a7d7ef5f29a37d0ac226b0a1429a469ad1/tests/ws_link_semver/.gitkeep -------------------------------------------------------------------------------- /tests/ws_link_semver/atlas.config: -------------------------------------------------------------------------------- 1 | { 2 | "deps": "", 3 | "nameOverrides": { 4 | "proj$+": "https://example.com/buildGraph/proj$#" 5 | }, 6 | "urlOverrides": {}, 7 | "plugins": "", 8 | "resolver": "SemVer", 9 | "graph": null 10 | } -------------------------------------------------------------------------------- /tests/ws_link_semver/ws_link_semver.nimble: -------------------------------------------------------------------------------- 1 | requires "proj_a" 2 | -------------------------------------------------------------------------------- /tests/ws_minproject1/.gitignore: -------------------------------------------------------------------------------- 1 | *.nimble 2 | -------------------------------------------------------------------------------- /tests/ws_minproject1/atlas.config: -------------------------------------------------------------------------------- 1 | { 2 | "deps": "", 3 | "nameOverrides": { 4 | "proj$+": "http://example.com/buildGraph/proj$#", 5 | }, 6 | "plugins": "", 7 | "resolver": "MinVer", 8 | "graph": null 9 | } 10 | -------------------------------------------------------------------------------- /tests/ws_semproject1/.gitignore: -------------------------------------------------------------------------------- 1 | *.nimble 2 | -------------------------------------------------------------------------------- /tests/ws_semproject1/atlas.config: -------------------------------------------------------------------------------- 1 | { 2 | "deps": "", 3 | "nameOverrides": { 4 | "proj$+": "http://example.com/buildGraph/proj$#", 5 | }, 6 | "plugins": "", 7 | "resolver": "SemVer", 8 | "graph": null 9 | } 10 | -------------------------------------------------------------------------------- /tests/ws_semproject2/.gitignore: -------------------------------------------------------------------------------- 1 | *.nimble 2 | -------------------------------------------------------------------------------- /tests/ws_semproject2/atlas.config: -------------------------------------------------------------------------------- 1 | { 2 | "deps": "", 3 | "nameOverrides": { 4 | "proj$+": "http://example.com/buildGraphNoGitTags/proj$#", 5 | }, 6 | "plugins": "", 7 | "resolver": "SemVer", 8 | "graph": null 9 | } 10 | -------------------------------------------------------------------------------- /tests/ws_semver/atlas.config: -------------------------------------------------------------------------------- 1 | { 2 | "deps": "", 3 | "overrides": "url.rules", 4 | "plugins": "", 5 | "resolver": "SemVer", 6 | "graph": null 7 | } -------------------------------------------------------------------------------- /tests/ws_semver/expected/deps.dot: -------------------------------------------------------------------------------- 1 | digraph deps { 2 | "file://./source/F/#head" [label=""]; 3 | "file://./source/E/2.0.0" [label=""]; 4 | "file://./source/D/1.0.0" [label=""]; 5 | "file://./source/C/1.0" [label=""]; 6 | "file://./source/F/#head" -> "file://./source/E/2.0.0"; 7 | "file://./source/E/2.0.0" -> "file://./source/D/1.0.0"; 8 | "file://./source/D/1.0.0" -> "file://./source/C/1.0"; 9 | } 10 | -------------------------------------------------------------------------------- /tests/ws_semver/expected/myproject.nimble: -------------------------------------------------------------------------------- 1 | requires "F" 2 | -------------------------------------------------------------------------------- /tests/ws_semver/expected/nim.cfg: -------------------------------------------------------------------------------- 1 | ############# begin Atlas config section ########## 2 | --noNimblePath 3 | --path:"../F" 4 | --path:"../E" 5 | --path:"../D" 6 | --path:"../C" 7 | ############# end Atlas config section ########## 8 | -------------------------------------------------------------------------------- /tests/ws_semver/url.rules: -------------------------------------------------------------------------------- 1 | $+ -> file://./source/$# 2 | -------------------------------------------------------------------------------- /tests/ws_semver2/atlas.config: -------------------------------------------------------------------------------- 1 | { 2 | "deps": "", 3 | "overrides": "url.rules", 4 | "plugins": "", 5 | "resolver": "SemVer", 6 | "graph": null 7 | } -------------------------------------------------------------------------------- /tests/ws_semver2/url.rules: -------------------------------------------------------------------------------- 1 | proj$+ -> http://example.com/buildGraph/proj$# 2 | -------------------------------------------------------------------------------- /tests/ws_semver_unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nim-lang/atlas/40db39a7d7ef5f29a37d0ac226b0a1429a469ad1/tests/ws_semver_unit/.gitkeep -------------------------------------------------------------------------------- /tests/ws_semver_unit/atlas.config: -------------------------------------------------------------------------------- 1 | { 2 | "deps": "", 3 | "nameOverrides": { 4 | "proj$+": "https://example.com/buildGraph/proj$#" 5 | }, 6 | "urlOverrides": {}, 7 | "plugins": "", 8 | "resolver": "SemVer", 9 | "graph": null 10 | } -------------------------------------------------------------------------------- /tests/ws_semver_unit/ws_semver_unit.nimble: -------------------------------------------------------------------------------- 1 | requires "proj_a" 2 | -------------------------------------------------------------------------------- /tests/ws_testtraverse/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nim-lang/atlas/40db39a7d7ef5f29a37d0ac226b0a1429a469ad1/tests/ws_testtraverse/.gitkeep -------------------------------------------------------------------------------- /tests/ws_testtraverse/atlas.config: -------------------------------------------------------------------------------- 1 | { 2 | "deps": "", 3 | "nameOverrides": { 4 | "$+": "file://./buildGraph/$#" 5 | }, 6 | "urlOverrides": {}, 7 | "plugins": "", 8 | "resolver": "SemVer", 9 | "graph": null 10 | } -------------------------------------------------------------------------------- /tests/ws_testtraverse/ws_testtraverse.nimble: -------------------------------------------------------------------------------- 1 | requires "proj_a" 2 | -------------------------------------------------------------------------------- /tests/ws_testtraverse_explicit/.gitignore: -------------------------------------------------------------------------------- 1 | ws_testtraverse_explicit.nimble 2 | -------------------------------------------------------------------------------- /tests/ws_testtraverse_explicit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nim-lang/atlas/40db39a7d7ef5f29a37d0ac226b0a1429a469ad1/tests/ws_testtraverse_explicit/.gitkeep -------------------------------------------------------------------------------- /tests/ws_testtraverse_explicit/atlas.config: -------------------------------------------------------------------------------- 1 | { 2 | "deps": "", 3 | "nameOverrides": { 4 | "$+": "file://./buildGraph/$#" 5 | }, 6 | "urlOverrides": {}, 7 | "plugins": "", 8 | "resolver": "SemVer", 9 | "graph": null 10 | } -------------------------------------------------------------------------------- /tests/ws_testtraverselinked/ws_testtraverselinked.nimble: -------------------------------------------------------------------------------- 1 | version = "0.1.0" 2 | srcDir = "." 3 | requires "ws_testtraverse" --------------------------------------------------------------------------------