├── .github └── workflows │ └── release_build.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── analytics.md ├── bin └── .gitkeep ├── changelog.markdown ├── choosenim.nimble ├── readme.md ├── scripts ├── build.sh ├── choosenim-unix-init.sh └── runme.bat ├── src ├── choosenim.nim ├── choosenim.nims └── choosenimpkg │ ├── builder.nim │ ├── channel.nim │ ├── cliparams.nim │ ├── common.nim │ ├── download.nim │ ├── env.nim │ ├── proxyexe.nim │ ├── proxyexe.nim.cfg │ ├── proxyexe.nims │ ├── switcher.nim │ ├── telemetry.nim │ ├── utils.nim │ └── versions.nim └── tests └── tester.nim /.github/workflows/release_build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will automatically upload a binary artifact when a release/tag is created 2 | name: Build and upload binary 3 | 4 | on: 5 | # allow to build manually 6 | workflow_dispatch: 7 | # build automatically when pushing a tag 8 | push: 9 | branches: 10 | - "!*" 11 | tags: 12 | - "v*" 13 | 14 | jobs: 15 | # ---------------------------------------------------------------------------- 16 | # this will checkout and build nim stable from gh repository on manylinux2014 / CentOS 7 17 | build-linux: 18 | runs-on: ubuntu-latest 19 | container: 20 | image: quay.io/pypa/manylinux2014_x86_64 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Build binary 24 | run: | 25 | CHOOSENIM_DIR=`pwd` 26 | # checking out and compiling nim last stable from git tag 27 | mkdir -p nimDir 28 | STABLE_NIM=`curl -sSL http://nim-lang.org/channels/stable | xargs` 29 | git clone --depth 1 --branch v$STABLE_NIM https://github.com/nim-lang/Nim.git nimDir 30 | cd nimDir 31 | sh build_all.sh 32 | bin/nim c koch 33 | ./koch boot -d:release 34 | ./koch tools 35 | PATH=$PATH:`pwd`/bin 36 | # compile choosenim 37 | cd $CHOOSENIM_DIR 38 | nimble install -y 39 | nimble build 40 | ls bin/* 41 | 42 | - name: Upload binaries to release/tag 43 | uses: svenstaro/upload-release-action@v2 44 | with: 45 | repo_token: ${{ secrets.GITHUB_TOKEN }} 46 | overwrite: true 47 | tag: ${{ github.ref }} 48 | asset_name: choosenim-manylinux2014 49 | file: ${{ runner.workspace }}/choosenim/bin/choosenim 50 | 51 | # ---------------------------------------------------------------------------- 52 | # this uses choosenim by itself - you may need to build manually if you break choosenim 53 | build-win32: 54 | runs-on: windows-latest 55 | steps: 56 | - uses: actions/checkout@v2 57 | - uses: iffy/install-nim@v4.0.1 58 | - name: Build binary 59 | run: | 60 | nimble install -y 61 | nimble build 62 | dir bin/* 63 | 64 | - name: Upload binaries to release/tag 65 | uses: svenstaro/upload-release-action@v2 66 | with: 67 | repo_token: ${{ secrets.GITHUB_TOKEN }} 68 | overwrite: true 69 | tag: ${{ github.ref }} 70 | asset_name: choosenim-windows 71 | file: ${{ runner.workspace }}/choosenim/bin/choosenim.exe 72 | 73 | # ---------------------------------------------------------------------------- 74 | # this uses choosenim by itself - you may need to build manually if you break choosenim 75 | build-macos: 76 | runs-on: macos-latest 77 | steps: 78 | - uses: actions/checkout@v2 79 | - uses: iffy/install-nim@v4.0.1 80 | - name: Build binary 81 | run: | 82 | nimble install -y 83 | nimble build 84 | ls bin/* 85 | 86 | - name: Upload binaries to release/tag 87 | uses: svenstaro/upload-release-action@v2 88 | with: 89 | repo_token: ${{ secrets.GITHUB_TOKEN }} 90 | overwrite: true 91 | tag: ${{ github.ref }} 92 | asset_name: choosenim-macos 93 | file: ${{ runner.workspace }}/choosenim/bin/choosenim 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache 2 | bin/choosenim 3 | src/choosenimpkg/proxyexe 4 | *.exe 5 | 6 | tests/nimcache 7 | tests/choosenimDir 8 | tests/nimbleDir 9 | tests/tester 10 | 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | - osx 4 | - windows 5 | 6 | language: c 7 | 8 | env: 9 | - BRANCH=1.2.8 10 | 11 | cache: 12 | directories: 13 | - "$HOME/.choosenim" 14 | 15 | addons: 16 | apt: 17 | update: true 18 | packages: 19 | - musl-tools 20 | 21 | # Don't rebuild autotagged 22 | branches: 23 | except: 24 | - /^v[0-9.]+-[0-9]+-[0-9a-f]+$/ 25 | 26 | script: 27 | - source ./scripts/build.sh 28 | 29 | notifications: 30 | irc: "chat.freenode.net#nimbuild" 31 | 32 | before_deploy: 33 | - git config --local user.name "${GIT_TAG_USER_NAME}" 34 | - git config --local user.email "${GIT_TAG_USER_EMAIL}" 35 | 36 | deploy: 37 | - provider: releases 38 | api_key: "${GITHUB_OAUTH_TOKEN}" 39 | file_glob: true 40 | file: bin/choosenim-* 41 | skip_cleanup: true 42 | overwrite: true 43 | prerelease: false 44 | on: 45 | tags: true 46 | condition: "-z ${PRERELEASE+x}" 47 | - provider: releases 48 | api_key: "${GITHUB_OAUTH_TOKEN}" 49 | file_glob: true 50 | file: bin/choosenim-* 51 | skip_cleanup: true 52 | overwrite: true 53 | prerelease: true 54 | on: 55 | tags: true 56 | condition: "! -z ${PRERELEASE+x}" 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, Dominik Picheta 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /analytics.md: -------------------------------------------------------------------------------- 1 | # Anonymous gathering of user analytics 2 | 3 | Starting with version 0.3.0, choosenim has the ability to gather anonymous 4 | aggregate user behaviour analytics and to report them to Google Analytics. 5 | 6 | This is entirely optional and is "opt-neutral", that is choosenim will 7 | ask you to decide whether you want to participate in this data gathering 8 | without offering a default option. You must choose. 9 | 10 | ## Why? 11 | 12 | This is the most straightforward way to gather information about Nim's users 13 | directly. It allows us to be aware of the platforms where Nim is being used 14 | and any installation issues that our users are facing. 15 | 16 | Overall the data we collect allows us to prioritise fixes and features based on 17 | how people are using choosenim. For example: 18 | 19 | * If there is a high exception count on specific platforms, we can prioritise 20 | fixing it for the next release. 21 | * Collecting the OS version allows us to decide which platforms to prioritise 22 | and support. 23 | 24 | ## What is collected? 25 | 26 | At a high level we currently collect a number of events, exception counts and 27 | build and download timings. 28 | 29 | To be more specific, we record the following information: 30 | 31 | * OS information, for example: ``Mac OS X v10.11 El Capitan`` or 32 | ``Linux 4.11.6-041106-generic x86_64``. 33 | * The command-line arguments passed to choosenim, for example 34 | ``choosenim --nimbleDir:~/myNimbleDir stable``. 35 | * Events when a build is started, when it fails and when it succeeds. 36 | * Build time in seconds and download time in seconds (including the 37 | URL that was downloaded). 38 | * The choosenim version. 39 | 40 | For each user a new UUID is generated so there is no way for us or Google 41 | to identify you. The UUID is used to measure user counts. 42 | 43 | ## Where is the data sent? 44 | 45 | The recorded data is sent to Google Analytics over HTTPS. 46 | 47 | ## Who has access? 48 | 49 | The analytics are currently only accessible to the maintainers of choosenim. 50 | At the minute this only includes [@dom96](https://github.com/dom96). 51 | 52 | Summaries of the data may be released in the future to the public. 53 | 54 | ## Where is the code? 55 | 56 | The code is viewable in [telemetry.nim](https://github.com/dom96/choosenim/blob/master/src/choosenimpkg/telemetry.nim). 57 | 58 | The reporting is done asynchronously and will fail fast to avoid any 59 | delay in execution. 60 | 61 | ## Opting in 62 | 63 | To opt-in simply answer "yes" or "y" to the following question: 64 | 65 | ``` 66 | Prompt: Can choosenim record and send anonymised telemetry data? [y/n] 67 | ... Anonymous aggregate user analytics allow us to prioritise 68 | ... fixes and features based on how, where and when people use Nim. 69 | ... For more details see: https://goo.gl/NzUEPf. 70 | Answer: 71 | ``` 72 | 73 | If you answer "no" and then change your mind, you can always have choosenim 74 | ask you again by removing the ``analytics`` file in the choosenim directory, 75 | usually ``rm ~/.choosenim/analytics`` should do the trick. 76 | 77 | ## Opting out 78 | 79 | Choosenim analytics help us and leaving them on is appreciated. However, 80 | we understand if you don't feel comfortable having them on. 81 | 82 | To opt out simply answer "no" or "n" to the following question: 83 | 84 | ``` 85 | Prompt: Can choosenim record and send anonymised telemetry data? [y/n] 86 | ... Anonymous aggregate user analytics allow us to prioritise 87 | ... fixes and features based on how, where and when people use Nim. 88 | ... For more details see: https://goo.gl/NzUEPf. 89 | Answer: 90 | ``` 91 | 92 | You can also set the ``CHOOSENIM_NO_ANALYTICS`` or ``DO_NOT_TRACK`` variable in your environment: 93 | 94 | ``` 95 | export CHOOSENIM_NO_ANALYTICS=1 96 | # or 97 | export DO_NOT_TRACK=1 98 | ``` 99 | -------------------------------------------------------------------------------- /bin/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /changelog.markdown: -------------------------------------------------------------------------------- 1 | # Choosenim changelog 2 | 3 | ## 0.8.4 - 06/07/2022 4 | 5 | This is minor release to resolve issues with OpenSSL on newer Linux distributions. 6 | 7 | See the full list of changes here: 8 | 9 | https://github.com/dom96/choosenim/compare/v0.8.2...0.8.4 10 | 11 | ## 0.8.2 - 05/10/2021 12 | 13 | This is mainly a bug fix release to resolve a few regressions and other bugs in 14 | 0.8.0. 15 | 16 | This release also includes a new feature: you can now get the path of the 17 | selected Nim toolchain via `choosenim show path`. 18 | 19 | See the full list of changes here: 20 | 21 | https://github.com/dom96/choosenim/compare/v0.8.0...0.8.2 22 | 23 | ## 0.8.0 - 02/10/2021 24 | 25 | This is a major new release containing many new significant improvements. 26 | In particular: 27 | 28 | * unxz/tar and the Nim zippy library are now used for extracting archives. 29 | * puppy is used for downloading via HTTP on Windows. 30 | * curl is used again for downloading via HTTP on macOS. 31 | * fixed issues building new Nim source code that relies on the new `build_all` 32 | scripts. 33 | * choosenim binaries no longer rely on musl. 34 | * better handling of antivirus false positives. 35 | 36 | See the full list of changes here: 37 | 38 | https://github.com/dom96/choosenim/compare/v0.7.4...0.8.0 39 | 40 | ## 0.7.4 - 20/10/2020 41 | 42 | This is a bug fix release to resolve a regression where a spurious `pkgs` 43 | directory was being created when any choosenim shim was executed. 44 | 45 | Once choosenim is upgraded, simply switch Nim versions and the shims will be 46 | regenerated, solving the issue. 47 | 48 | ## 0.7.2 - 17/10/2020 49 | 50 | This is a bug fix release to resolve a regression caused by changes in Nimble 51 | which prevented choosenim from finding the Nimble directory. 52 | 53 | ## 0.7.0 - 16/10/2020 54 | 55 | The major new feature is that all builds are now static. There should be no 56 | runtime dependencies for choosenim anymore. 57 | 58 | Changes: 59 | 60 | * A critical bug was fixed where choosenim would fail if existng DLLs were present. 61 | * The `update` command will now always change to the newly installed version. 62 | In previous versions this would only happen when the currently selected 63 | channel is updated. 64 | * A new `remove` command is now available. 65 | * The `nim-gdb` utility is now shimmed. 66 | * Various small bug fixes, #203 and #195. 67 | * The `GITHUB_TOKEN` env var will now be used if present for certain actions. 68 | * Better messages when downloading nightlies. 69 | 70 | ## 0.6.0 - 06/03/2020 71 | 72 | The major new feature is default installation of 64-bit Nim 73 | binaries on Windows. 74 | 75 | Changes: 76 | * Install latest nightly build of Nim on `choosenim devel` and 77 | `choosenim update devel` 78 | * Install latest devel commit instead of nightlies with the 79 | `--latest` flag 80 | * Git based update for `choosenim update devel --latest` instead 81 | of deleting, downloading and bootstrapping from scratch 82 | * Optionally add `~/.nimble/bin` to PATH on Windows when using the 83 | `--firstInstall` flag 84 | * Fix `choosenim update self` failure on Windows 85 | * Fix crash where shims could not be rewritten when in use 86 | * Fix crash on OSX due to an openssl version conflict 87 | 88 | See the full list of changes here: 89 | 90 | https://github.com/dom96/choosenim/compare/v0.5.1...v0.6.0 91 | 92 | ## 0.5.1 - 15/01/2020 93 | 94 | Includes multiple bug fixes and minor improvements. 95 | 96 | * Create a shim for testament 97 | * Ship x64 binaries for Windows 98 | * Delete downloaded archives and csources directory after successful 99 | installation to save disk space 100 | * Error if C compiler is not found rather than just warning 101 | * Extract Nim binaries with execute permissions 102 | * Enable installation using `nimble install choosenim` 103 | 104 | See the full list of changes here: 105 | 106 | https://github.com/dom96/choosenim/compare/v0.5.0...v0.5.1 107 | 108 | ## 0.5.0 - 14/11/2019 109 | 110 | The major new feature is the use of nimarchive and 111 | support for Linux binary builds. 112 | 113 | See the full list of changes here: 114 | 115 | https://github.com/dom96/choosenim/compare/v0.4.0...v0.5.0 116 | 117 | ## 0.4.0 - 18/04/2019 118 | 119 | The major new features include Windows 64-bit support, the installation 120 | of Windows binaries and the `versions` command. 121 | 122 | See the full list of changes here: 123 | 124 | https://github.com/dom96/choosenim/compare/v0.3.2...v0.4.0 125 | 126 | ## 0.3.2 - 27/02/2018 127 | 128 | The major new feature in this release is the ability for choosenim to 129 | update itself, this is done by executing ``choosenim update self``. 130 | 131 | * A bug where choosenim would fail because of an existing .tar file 132 | was fixed. 133 | * Proxy support implemented. 134 | * Fixes #17 and #51. 135 | 136 | ## 0.3.0 - 22/09/2017 137 | 138 | The major new feature in this release is the ability to record analytics. 139 | For more information see the 140 | [analytics document](https://github.com/dom96/choosenim/blob/master/analytics.md). 141 | 142 | * On Linux a .tar.xz archive will now be downloaded instead of the larger 143 | .tar.gz archive. This means that choosenim depends on `unxz` on Linux. 144 | * Improve messages during the first installation. 145 | 146 | ## 0.2.2 - 17/05/2017 147 | 148 | Includes two bug fixes. 149 | 150 | * The exit codes are now handled correctly for proxied executables. 151 | * Choosenim now checks for the presence of a `lib` directory inside 152 | ``~/.nimble`` and offers to remove it. 153 | (Issue [#13](https://github.com/dom96/choosenim/issues/13)) 154 | 155 | ## 0.2.0 - 09/05/2017 156 | 157 | Includes multiple bug fixes and some improvements. 158 | 159 | * Implements warning when Nimble's version is lower than 0.8.6. (Issue 160 | [#10](https://github.com/dom96/choosenim/issues/10)) 161 | * Improves choosenim unix init script to support stdin properly. 162 | * Fixes invalid condition for checking the presence of a C compiler. 163 | * Fixes relative paths not being expanded when they are selected by a user. 164 | * Fixes problem with updating a version not a channel. 165 | 166 | ---- 167 | 168 | Full changelog: https://github.com/dom96/choosenim/compare/v0.1.0...v0.2.0 169 | -------------------------------------------------------------------------------- /choosenim.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.8.4" 4 | author = "Dominik Picheta" 5 | description = "The Nim toolchain installer." 6 | license = "BSD" 7 | 8 | srcDir = "src" 9 | binDir = "bin" 10 | bin = @["choosenim"] 11 | 12 | skipExt = @["nim"] 13 | 14 | # Dependencies 15 | 16 | # Note: https://github.com/dom96/choosenim/issues/233 (need to resolve when updating Nimble) 17 | requires "nim >= 1.2.6", "nimble#8f7af86" 18 | when defined(macosx): 19 | requires "libcurl >= 1.0.0" 20 | requires "analytics >= 0.3.0" 21 | requires "osinfo >= 0.3.0" 22 | requires "zippy >= 0.7.2" 23 | when defined(windows): 24 | requires "puppy 1.5.4" 25 | 26 | task release, "Build a release binary": 27 | exec "nimble build -d:release" 28 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # choosenim 2 | 3 | choosenim installs the [Nim programming language](https://nim-lang.org) from 4 | official downloads and sources, enabling you to easily switch between stable 5 | and development compilers. 6 | 7 | The aim of this tool is two-fold: 8 | 9 | * Provide an easy way to install the Nim compiler and tools. 10 | * Manage multiple Nim installations and allow them to be selected on-demand. 11 | 12 | ## Typical usage 13 | 14 | To select the current `stable` release of Nim: 15 | 16 | ```bash 17 | $ choosenim stable 18 | Installed component 'nim' 19 | Installed component 'nimble' 20 | Installed component 'nimgrep' 21 | Installed component 'nimpretty' 22 | Installed component 'nimsuggest' 23 | Installed component 'testament' 24 | Switched to Nim 1.0.0 25 | $ nim -v 26 | Nim Compiler Version 1.0.0 [Linux: amd64] 27 | ``` 28 | 29 | To update to the latest `stable` release of Nim: 30 | 31 | ```bash 32 | $ choosenim update stable 33 | ``` 34 | 35 | To display which versions are currently installed: 36 | 37 | ```bash 38 | $ choosenim show 39 | Selected: 1.6.6 40 | Channel: stable 41 | Path: /home/dom/.choosenim/toolchains/nim-1.6.6 42 | 43 | Versions: 44 | #devel 45 | * 1.6.6 46 | 1.0.0 47 | #v1.0.0 48 | ``` 49 | 50 | Versions can be selected via `choosenim 1.6.6` or by branch/tag name via `choosenim #devel` (note that selecting branches is likely to require Nim to be bootstrapped which may be slow). 51 | 52 | ## Installation 53 | 54 | ### Windows 55 | 56 | Download the latest Windows version from the 57 | [releases](https://github.com/dom96/choosenim/releases) page (the .zip file, for example here is [``v0.7.4``](https://github.com/dom96/choosenim/releases/download/v0.7.4/choosenim-0.7.4_windows_amd64.zip)). 58 | 59 | Extract the zip archive and run the ``runme.bat`` script. Follow any on screen 60 | prompts and enjoy your new Nim and choosenim installation. 61 | 62 | ---- 63 | 64 | There is also a third-party project to provide an installer for choosenim, 65 | you can find it [here](https://gitlab.com/ArMour85/choosenim-setup) (note that 66 | this isn't vetted by the Nim team so you do so at your own risk). 67 | 68 | ### Unix 69 | 70 | ``` 71 | curl https://nim-lang.org/choosenim/init.sh -sSf | sh 72 | ``` 73 | ``` 74 | wget -qO - https://nim-lang.org/choosenim/init.sh | sh 75 | ``` 76 | 77 | **Optional:** You can specify the initial version you would like the `init.sh` 78 | script to install by specifying the ``CHOOSENIM_CHOOSE_VERSION`` 79 | environment variable. 80 | 81 | ## How choosenim works 82 | 83 | Similar to the likes of ``rustup`` and ``pyenv``, ``choosenim`` is a 84 | _toolchain multiplexer_. It installs and manages multiple Nim toolchains and 85 | presents them all through a single set of tools installed in ``~/.nimble/bin``. 86 | 87 | The ``nim``, ``nimble`` and other tools installed in ``~/.nimble/bin`` are 88 | proxies that delegate to the real toolchain. ``choosenim`` then allows you 89 | to change the active toolchain by reconfiguring the behaviour of the proxies. 90 | 91 | The toolchains themselves are installed into ``~/.choosenim/toolchains``. For 92 | example running ``nim`` will execute the proxy in ``~/.nimble/bin/nim``, which 93 | in turn will run the compiler in ``~/.choosenim/toolchains/nim-1.0.0/bin/nim``, 94 | assuming that 1.0.0 was selected. 95 | 96 | ### How toolchains are installed 97 | 98 | ``choosenim`` downloads and installs the official release 99 | [binaries](https://nim-lang.org/install.html) on Windows and Linux. On other 100 | platforms, the official source [release](https://nim-lang.org/install_unix.html) 101 | is downloaded and built. This operation is only performed once when a new 102 | version is selected. 103 | 104 | As official binaries are made available for more platforms, ``choosenim`` will 105 | install them accordingly. 106 | 107 | ## Dependencies 108 | 109 | | | Windows | Linux | macOS (*) | 110 | |------------|:-----------------------------:|:-----------------------:|:---------------------:| 111 | | C compiler | *Downloaded automatically* | gcc/clang | gcc/clang | 112 | | OpenSSL | N/A | N/A | N/A | 113 | | curl | N/A | Any recent version (※) | Any recent version | 114 | 115 | \* Many macOS dependencies should already be installed. You may need to install 116 | a C compiler however. More information on dependencies is available 117 | [here](https://nim-lang.org/install_unix.html). 118 | 119 | ※ Some users needed to install `libcurl4-gnutls-dev` (see [here](https://github.com/dom96/choosenim/issues/303)) 120 | 121 | Git is required when installing #HEAD or a specific commit of Nim. The `unxz` 122 | binary is optional but will allow choosenim to download the smallest tarballs. 123 | 124 | ## Usage 125 | 126 | ``` 127 | > choosenim -h 128 | choosenim: The Nim toolchain installer. 129 | 130 | Choose a job. Choose a mortgage. Choose life. Choose Nim. 131 | 132 | Usage: 133 | choosenim 134 | 135 | Example: 136 | choosenim 1.0.0 137 | Installs (if necessary) and selects version 0.16.0 of Nim. 138 | choosenim stable 139 | Installs (if necessary) Nim from the stable channel (latest stable release) 140 | and then selects it. 141 | choosenim #head 142 | Installs (if necessary) and selects the latest current commit of Nim. 143 | Warning: Your shell may need quotes around `#head`: choosenim "#head". 144 | choosenim ~/projects/nim 145 | Selects the specified Nim installation. 146 | choosenim update stable 147 | Updates the version installed on the stable release channel. 148 | choosenim versions [--installed] 149 | Lists the available versions of Nim that choosenim has access to. 150 | 151 | Channels: 152 | stable 153 | Describes the latest stable release of Nim. 154 | devel 155 | Describes the latest development (or nightly) release of Nim taken from 156 | the devel branch. 157 | 158 | Commands: 159 | update Installs the latest release of the specified 160 | version or channel. 161 | show Displays the selected version and channel. 162 | show path Prints only the path of the current Nim version. 163 | update self Updates choosenim itself. 164 | versions [--installed] Lists available versions of Nim, passing 165 | `--installed` only displays versions that 166 | are installed locally (no network requests). 167 | 168 | Options: 169 | -h --help Show this output. 170 | -y --yes Agree to every question. 171 | --version Show version. 172 | --verbose Show low (and higher) priority output. 173 | --debug Show debug (and higher) priority output. 174 | --noColor Don't colorise output. 175 | 176 | --choosenimDir: Specify the directory where toolchains should be 177 | installed. Default: ~/.choosenim. 178 | --nimbleDir: Specify the Nimble directory where binaries will be 179 | placed. Default: ~/.nimble. 180 | --firstInstall Used by install script. 181 | ``` 182 | 183 | ## Analytics 184 | 185 | Check out the 186 | [analytics](https://github.com/dom96/choosenim/blob/master/analytics.md) 187 | document for details. 188 | 189 | ## License 190 | 191 | MIT 192 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # Use common Travis script - https://github.com/genotrance/nim-travis 4 | curl https://raw.githubusercontent.com/genotrance/nim-travis/master/travis.sh -LsSf -o travis.sh 5 | source travis.sh 6 | 7 | # Skip building autotagged version 8 | export COMMIT_TAG=`git tag --points-at HEAD | head -n 1` 9 | export COMMIT_HASH=`git rev-parse --short HEAD` 10 | export CURRENT_BRANCH="${TRAVIS_BRANCH}" 11 | echo "Commit tag: ${COMMIT_TAG}" 12 | echo "Commit hash: ${COMMIT_HASH}" 13 | echo "Current branch: ${CURRENT_BRANCH}" 14 | 15 | # Environment vars 16 | if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then 17 | export EXT=".exe" 18 | else 19 | export EXT="" 20 | fi 21 | 22 | if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then 23 | export OSNAME="macosx" 24 | else 25 | export OSNAME="$TRAVIS_OS_NAME" 26 | fi 27 | 28 | # Build release version 29 | nimble build -y -d:release -d:staticBuild 30 | 31 | # Set version and tag info 32 | export CHOOSENIM_VERSION="$(./bin/choosenim --version | cut -f2,2 -d' ' | sed 's/v//')" 33 | echo "Version: v${CHOOSENIM_VERSION}" 34 | if [[ -z "${COMMIT_TAG}" ]]; then 35 | # Create tag with date and hash, not an official tagged release 36 | export VERSION_TAG="${CHOOSENIM_VERSION}-$(date +'%Y%m%d')-${COMMIT_HASH}" 37 | if [[ "${CURRENT_BRANCH}" == "master" ]]; then 38 | # Deploy only on main branch 39 | export TRAVIS_TAG="v${VERSION_TAG}" 40 | export PRERELEASE=true 41 | fi 42 | elif [[ "${COMMIT_TAG}" == "v${CHOOSENIM_VERSION}" ]]; then 43 | # Official tagged release 44 | export VERSION_TAG="${CHOOSENIM_VERSION}" 45 | export TRAVIS_TAG="${COMMIT_TAG}" 46 | else 47 | # Other tag, mostly autotagged rebuild 48 | export VERSION_TAG="${COMMIT_TAG:1}" 49 | export TRAVIS_TAG="${COMMIT_TAG}" 50 | export PRERELEASE=true 51 | fi 52 | echo "Travis tag: ${TRAVIS_TAG}" 53 | echo "Prerelease: ${PRERELEASE}" 54 | export FILENAME="bin/choosenim-${VERSION_TAG}_${OSNAME}_${TRAVIS_CPU_ARCH}" 55 | echo "Filename: ${FILENAME}" 56 | 57 | # Run tests 58 | nimble test -d:release -d:staticBuild 59 | strip "bin/choosenim${EXT}" 60 | mv "bin/choosenim${EXT}" "${FILENAME}${EXT}" 61 | 62 | # Build debug version 63 | nimble build -g -d:staticBuild 64 | ./bin/choosenim${EXT} -v 65 | mv "bin/choosenim${EXT}" "${FILENAME}_debug${EXT}" 66 | -------------------------------------------------------------------------------- /scripts/choosenim-unix-init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright 2017 Dominik Picheta and Nim developers. 3 | # 4 | # Licensed under the BSD-3-Clause license. 5 | # 6 | # This script performs some platform detection, downloads the latest version 7 | # of choosenim and initiates its installation. 8 | 9 | set -u 10 | set -e 11 | 12 | url_prefix="https://github.com/dom96/choosenim/releases/download/" 13 | 14 | temp_prefix="${TMPDIR:-/tmp}" 15 | 16 | CHOOSE_VERSION="${CHOOSENIM_CHOOSE_VERSION:-stable}" 17 | 18 | need_tty=yes 19 | debug="" 20 | 21 | has_curl() { 22 | command -v curl >/dev/null 2>&1 23 | } 24 | 25 | has_wget() { 26 | command -v wget >/dev/null 2>&1 27 | } 28 | 29 | install() { 30 | get_platform || return 1 31 | local platform=$RET_VAL 32 | local stable_version= 33 | if has_curl; then 34 | stable_version=`curl -sSfL https://nim-lang.org/choosenim/stable` 35 | elif has_wget; then 36 | stable_version=`wget -qO - https://nim-lang.org/choosenim/stable` 37 | fi 38 | local filename="choosenim-$stable_version"_"$platform" 39 | local url="$url_prefix"v"$stable_version/$filename" 40 | local ext="" 41 | 42 | case $platform in 43 | *macosx_amd64* | *linux_amd64* ) 44 | ;; 45 | *windows_amd64* ) 46 | # Download ZIP for Windows 47 | local ext=".exe" 48 | local filename="$filename$ext" 49 | local url="$url$ext" 50 | ;; 51 | * ) 52 | say_err "Sorry, your platform ($platform) is not supported by choosenim." 53 | say_err "You will need to install Nim using an alternative method." 54 | say_err "See the following link for more info: https://nim-lang.org/install.html" 55 | exit 1 56 | ;; 57 | esac 58 | 59 | say "Downloading $filename" 60 | if has_curl; then 61 | curl -sSfL "$url" -o "$temp_prefix/$filename" 62 | elif has_wget; then 63 | wget -qO "$temp_prefix/$filename" "$url" 64 | fi 65 | chmod +x "$temp_prefix/$filename" 66 | 67 | if [ "$need_tty" = "yes" ]; then 68 | # The installer is going to want to ask for confirmation by 69 | # reading stdin. This script was piped into `sh` though and 70 | # doesn't have stdin to pass to its children. Instead we're going 71 | # to explicitly connect /dev/tty to the installer's stdin. 72 | if [ ! -t 1 ]; then 73 | err "Unable to run interactively. Run with -y to accept defaults." 74 | fi 75 | 76 | # Install Nim from desired channel. 77 | "$temp_prefix/$filename" $CHOOSE_VERSION --firstInstall ${debug} < /dev/tty 78 | else 79 | "$temp_prefix/$filename" $CHOOSE_VERSION --firstInstall -y ${debug} 80 | fi 81 | 82 | # Copy choosenim binary to Nimble bin. 83 | local nimbleBinDir=`"$temp_prefix/$filename" --getNimbleBin` 84 | cp "$temp_prefix/$filename" "$nimbleBinDir/choosenim$ext" 85 | say "ChooseNim installed in $nimbleBinDir" 86 | say "You must now ensure that the Nimble bin dir is in your PATH." 87 | if [ "$platform" != "windows_amd64" ]; then 88 | say "Place the following line in the ~/.profile or ~/.bashrc file." 89 | say " export PATH=$nimbleBinDir:\$PATH" 90 | case "${SHELL:=sh}" in 91 | *fish*) 92 | say "Running fish shell?" 93 | say "set -ga fish_user_paths $nimbleBinDir" 94 | ;; 95 | esac 96 | fi 97 | } 98 | 99 | get_platform() { 100 | # Get OS/CPU info and store in a `myos` and `mycpu` variable. 101 | local ucpu=`uname -m` 102 | local uos=`uname` 103 | local ucpu=`echo $ucpu | tr "[:upper:]" "[:lower:]"` 104 | local uos=`echo $uos | tr "[:upper:]" "[:lower:]"` 105 | 106 | case $uos in 107 | *linux* ) 108 | local myos="linux" 109 | ;; 110 | *dragonfly* ) 111 | local myos="freebsd" 112 | ;; 113 | *freebsd* ) 114 | local myos="freebsd" 115 | ;; 116 | *openbsd* ) 117 | local myos="openbsd" 118 | ;; 119 | *netbsd* ) 120 | local myos="netbsd" 121 | ;; 122 | *darwin* ) 123 | local myos="macosx" 124 | if [ "$HOSTTYPE" = "x86_64" ] ; then 125 | local ucpu="amd64" 126 | fi 127 | if [ "$HOSTTYPE" = "arm64" ] ; then 128 | # TODO: We don't have arm binaries for choosenim so far, rely on Rosetta. 129 | local ucpu="amd64" 130 | fi 131 | ;; 132 | *aix* ) 133 | local myos="aix" 134 | ;; 135 | *solaris* | *sun* ) 136 | local myos="solaris" 137 | ;; 138 | *haiku* ) 139 | local myos="haiku" 140 | ;; 141 | *mingw* | *msys* ) 142 | local myos="windows" 143 | ;; 144 | *) 145 | err "unknown operating system: $uos" 146 | ;; 147 | esac 148 | 149 | case $ucpu in 150 | *i386* | *i486* | *i586* | *i686* | *bepc* | *i86pc* ) 151 | local mycpu="i386" ;; 152 | *amd*64* | *x86-64* | *x86_64* ) 153 | local mycpu="amd64" ;; 154 | *sparc*|*sun* ) 155 | local mycpu="sparc" 156 | if [ "$(isainfo -b)" = "64" ]; then 157 | local mycpu="sparc64" 158 | fi 159 | ;; 160 | *ppc64* ) 161 | local mycpu="powerpc64" ;; 162 | *power*|*ppc* ) 163 | local mycpu="powerpc" ;; 164 | *mips* ) 165 | local mycpu="mips" ;; 166 | *arm*|*armv6l* ) 167 | local mycpu="arm" ;; 168 | *aarch64* ) 169 | local mycpu="arm64" ;; 170 | *) 171 | err "unknown processor: $ucpu" 172 | ;; 173 | esac 174 | 175 | RET_VAL="$myos"_"$mycpu" 176 | } 177 | 178 | say() { 179 | echo "choosenim-init: $1" 180 | } 181 | 182 | say_err() { 183 | say "Error: $1" >&2 184 | } 185 | 186 | err() { 187 | say_err "$1" 188 | exit 1 189 | } 190 | 191 | 192 | # check if we have to use /dev/tty to prompt the user 193 | while getopts "dy" opt; do 194 | case "$opt" in 195 | y) need_tty=no 196 | ;; 197 | d) debug="--debug" 198 | esac 199 | done 200 | 201 | install 202 | -------------------------------------------------------------------------------- /scripts/runme.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | .\choosenim\choosenim.exe stable --firstInstall 3 | 4 | for /f "delims=" %%a in ('.\choosenim\choosenim.exe --getNimbleBin') do @set NIMBLEBIN=%%a 5 | copy .\choosenim\choosenim.exe "%NIMBLEBIN%\choosenim.exe" 6 | 7 | echo Work finished. 8 | echo Now you must ensure that the Nimble bin dir is in your PATH: 9 | echo %NIMBLEBIN% 10 | 11 | pause -------------------------------------------------------------------------------- /src/choosenim.nim: -------------------------------------------------------------------------------- 1 | # Copyright (C) Dominik Picheta. All rights reserved. 2 | # BSD-3-Clause License. Look at license.txt for more info. 3 | import os, strutils, algorithm 4 | 5 | import nimblepkg/[cli, version] 6 | import nimblepkg/common as nimbleCommon 7 | from nimblepkg/packageinfo import getNameVersion 8 | 9 | import choosenimpkg/[download, builder, switcher, common, cliparams, versions] 10 | import choosenimpkg/[utils, channel, telemetry] 11 | 12 | when defined(windows): 13 | import choosenimpkg/env 14 | 15 | import times 16 | 17 | proc installVersion(version: Version, params: CliParams) = 18 | let 19 | extractDir = params.getInstallationDir(version) 20 | updated = gitUpdate(version, extractDir, params) 21 | 22 | if not updated: 23 | # Install the requested version. 24 | let path = download(version, params) 25 | defer: 26 | # Delete downloaded file 27 | discard tryRemoveFile(path) 28 | # Make sure no stale files from previous installation exist. 29 | removeDir(extractDir) 30 | # Extract the downloaded file. 31 | extract(path, extractDir) 32 | 33 | # A "special" version is downloaded from GitHub and thus needs a `.git` 34 | # directory in order to let `koch` know that it should download a "devel" 35 | # Nimble. 36 | if version.isSpecial: 37 | gitInit(version, extractDir, params) 38 | 39 | # Build the compiler 40 | build(extractDir, version, params) 41 | 42 | proc safeSwitchTo(version: Version, params: CliParams, wasInstalled: bool) = 43 | try: 44 | switchTo(version, params) 45 | except Exception as exc: 46 | # If we cannot switch to the newly installed version for whatever reason 47 | # we assume the installation failed. This can happen for example on 48 | # Windows when Windows Defender flags one of the binaries as a virus. 49 | display("Exception:", exc.msg, Error, HighPriority) 50 | # Perform clean up. 51 | if not wasInstalled and not params.skipClean: 52 | display("Cleaning", "failed install of " & $version, priority = HighPriority) 53 | try: 54 | removeDir(params.getInstallationDir(version)) 55 | except Exception as exc: 56 | display("Warning:", "Cleaning failed: " & exc.msg, Warning) 57 | 58 | # Report telemetry. 59 | report(initEvent(ErrorEvent, label=exc.msg), params) 60 | raise newException(ChooseNimError, "Installation failed") 61 | 62 | proc chooseVersion(version: string, params: CliParams) = 63 | # Command is a version. 64 | let version = parseVersion(version) 65 | 66 | # Verify that C compiler is installed. 67 | if params.needsCCInstall(): 68 | when defined(windows): 69 | # Install MingW. 70 | let path = downloadMingw(params) 71 | extract(path, getMingwPath(params)) 72 | else: 73 | let binName = 74 | when defined(macosx): 75 | "clang" 76 | else: 77 | "gcc" 78 | 79 | raise newException( 80 | ChooseNimError, 81 | "No C compiler found. Nim compiler requires a C compiler.\n" & 82 | "Install " & binName & " using your favourite package manager." 83 | ) 84 | 85 | # Verify that DLLs (openssl primarily) are installed. 86 | when defined(windows): 87 | if params.needsDLLInstall(): 88 | # Install DLLs. 89 | let 90 | path = downloadDLLs(params) 91 | tempDir = getTempDir() / "choosenim-dlls" 92 | binDir = getBinDir(params) 93 | removeDir(tempDir) 94 | createDir(tempDir) 95 | extract(path, tempDir) 96 | for kind, path in walkDir(tempDir, relative = true): 97 | if kind == pcFile: 98 | try: 99 | if not fileExists(binDir / path) or 100 | getLastModificationTime(binDir / path) < getLastModificationTime(tempDir / path): 101 | moveFile(tempDir / path, binDir / path) 102 | display("Info:", "Copied '$1' to '$2'" % [path, binDir], priority = HighPriority) 103 | except: 104 | display("Warning:", "Error copying '$1' to '$2': $3" % [path, binDir, getCurrentExceptionMsg()], Warning, priority = HighPriority) 105 | removeDir(tempDir) 106 | else: 107 | display("Info:", "DLLs already installed", priority = MediumPriority) 108 | 109 | var wasInstalled = params.isVersionInstalled(version) 110 | if not wasInstalled: 111 | installVersion(version, params) 112 | 113 | safeSwitchTo(version, params, wasInstalled) 114 | 115 | proc choose(params: CliParams) = 116 | if dirExists(params.command): 117 | # Command is a file path likely pointing to an existing Nim installation. 118 | switchTo(params.command, params) 119 | else: 120 | # Check for release channel. 121 | if params.command.isReleaseChannel(): 122 | let version = getChannelVersion(params.command, params) 123 | 124 | chooseVersion(version, params) 125 | pinChannelVersion(params.command, version, params) 126 | setCurrentChannel(params.command, params) 127 | else: 128 | chooseVersion(params.command, params) 129 | 130 | when defined(windows): 131 | # Check and add ~/.nimble/bin to PATH 132 | if not isNimbleBinInPath(params) and params.firstInstall: 133 | setNimbleBinPath(params) 134 | 135 | proc updateSelf(params: CliParams) = 136 | display("Updating", "choosenim", priority = HighPriority) 137 | 138 | let version = getChannelVersion("self", params, live=true).newVersion 139 | if not params.force and version <= chooseNimVersion.newVersion: 140 | display("Info:", "Already up to date at version " & chooseNimVersion, 141 | Success, HighPriority) 142 | return 143 | 144 | # https://stackoverflow.com/a/9163044/492186 145 | let tag = "v" & $version 146 | let filename = "choosenim-" & $version & "_" & hostOS & "_" & hostCPU.addFileExt(ExeExt) 147 | let url = "https://github.com/dom96/choosenim/releases/download/$1/$2" % [ 148 | tag, filename 149 | ] 150 | let newFilename = getAppDir() / "choosenim_new".addFileExt(ExeExt) 151 | downloadFile(url, newFilename, params) 152 | 153 | let appFilename = getAppFilename() 154 | # Move choosenim.exe to choosenim_ver.exe 155 | let oldFilename = "choosenim_" & chooseNimVersion.addFileExt(ExeExt) 156 | display("Info:", "Renaming '$1' to '$2'" % [appFilename, oldFilename]) 157 | moveFile(appFilename, getAppDir() / oldFilename) 158 | 159 | # Move choosenim_new.exe to choosenim.exe 160 | display("Info:", "Renaming '$1' to '$2'" % [newFilename, appFilename]) 161 | moveFile(newFilename, appFilename) 162 | 163 | display("Info:", "Setting +x on downloaded file") 164 | inclFilePermissions(appFilename, {fpUserExec, fpGroupExec}) 165 | 166 | display("Info:", "Updated choosenim to version " & $version, 167 | Success, HighPriority) 168 | 169 | proc update(params: CliParams) = 170 | if params.commands.len != 2: 171 | raise newException(ChooseNimError, 172 | "Expected 1 parameter to 'update' command") 173 | 174 | let channel = params.commands[1] 175 | if channel.toLowerAscii() == "self": 176 | updateSelf(params) 177 | return 178 | 179 | display("Updating", channel, priority = HighPriority) 180 | 181 | # Retrieve the current version for the specified channel. 182 | let version = getChannelVersion(channel, params, live=true).newVersion 183 | 184 | # Ensure that the version isn't already installed. 185 | if not canUpdate(version, params): 186 | display("Info:", "Already up to date at version " & $version, 187 | Success, HighPriority) 188 | pinChannelVersion(channel, $version, params) 189 | if getSelectedVersion(params) != version: 190 | switchTo(version, params) 191 | return 192 | 193 | # Make sure the archive is downloaded again if the version is special. 194 | if version.isSpecial: 195 | removeDir(params.getDownloadPath($version).splitFile.dir) 196 | 197 | # Install the new version and pin it. 198 | installVersion(version, params) 199 | pinChannelVersion(channel, $version, params) 200 | 201 | display("Updated", "to " & $version, Success, HighPriority) 202 | 203 | # Always switch to the updated version. 204 | safeSwitchTo(version, params, wasInstalled=false) 205 | 206 | proc show(params: CliParams) = 207 | let channel = getCurrentChannel(params) 208 | let path = getSelectedPath(params) 209 | let (_, version) = getNameVersion(path) 210 | if params.commands.len == 2: 211 | let whatToShow = params.commands[1] 212 | if whatToShow.toLowerAscii() == "path": 213 | echo path 214 | return 215 | if version != "": 216 | display("Selected:", version, priority = HighPriority) 217 | 218 | if channel.len > 0: 219 | display("Channel:", channel, priority = HighPriority) 220 | else: 221 | display("Channel:", "No channel selected", priority = HighPriority) 222 | 223 | display("Path:", path, priority = HighPriority) 224 | 225 | var versions: seq[string] = @[] 226 | for path in walkDirs(params.getInstallDir() & "/*"): 227 | let (_, versionAvailable) = getNameVersion(path) 228 | versions.add(versionAvailable) 229 | 230 | if versions.len() > 1: 231 | versions.sort(system.cmp, Descending) 232 | if versions.contains("#head"): 233 | versions.del(find(versions, "#head")) 234 | versions.insert("#head", 0) 235 | if versions.contains("#devel"): 236 | versions.del(find(versions, "#devel")) 237 | versions.insert("#devel", 0) 238 | 239 | echo "" 240 | display("Versions:", " ", priority = HighPriority) 241 | for ver in versions: 242 | if ver == version: 243 | display("*", ver, Success, HighPriority) 244 | else: 245 | display("", ver, priority = HighPriority) 246 | 247 | proc versions(params: CliParams) = 248 | let currentChannel = getCurrentChannel(params) 249 | let currentVersion = getCurrentVersion(params) 250 | 251 | let specialVersions = getSpecialVersions(params) 252 | let localVersions = getInstalledVersions(params) 253 | 254 | let remoteVersions = 255 | if params.onlyInstalled: @[] 256 | else: getAvailableVersions(params) 257 | 258 | proc isActiveTag(params: CliParams, version: Version): string = 259 | let tag = 260 | if version == currentVersion: "*" 261 | else: " " # must have non-zero length, or won't be displayed 262 | return tag 263 | 264 | proc isLatestTag(params: CliParams, version: Version): string = 265 | let tag = 266 | if isLatestVersion(params, version): " (latest)" 267 | else: "" 268 | return tag 269 | 270 | proc canUpdateTag(params: CliParams, channel: string): string = 271 | let version = getChannelVersion(channel, params, live = (not params.onlyInstalled)) 272 | let channelVersion = parseVersion(version) 273 | let tag = 274 | if canUpdate(channelVersion, params): " (update available!)" 275 | else: "" 276 | return tag 277 | 278 | #[ Display version information,now that it has been collected ]# 279 | 280 | if currentChannel.len > 0: 281 | display("Channel:", currentChannel & canUpdateTag(params, currentChannel), priority = HighPriority) 282 | echo "" 283 | 284 | # local versions 285 | display("Installed:", " ", priority = HighPriority) 286 | for version in localVersions: 287 | let activeDisplay = 288 | if version == currentVersion: Success 289 | else: Message 290 | display(isActiveTag(params, version), $version & isLatestTag(params, version), activeDisplay, priority = HighPriority) 291 | for version in specialVersions: 292 | display(isActiveTag(params, version), $version, priority = HighPriority) 293 | echo "" 294 | 295 | # if the "--installed" flag was passed, don't display remote versions as we didn't fetch data for them. 296 | if (not params.onlyInstalled): 297 | display("Available:", " ", priority = HighPriority) 298 | for version in remoteVersions: 299 | if not (version in localVersions): 300 | display("", $version & isLatestTag(params, version), priority = HighPriority) 301 | echo "" 302 | 303 | proc remove(params: CliParams) = 304 | if params.commands.len != 2: 305 | raise newException(ChooseNimError, 306 | "Expected 1 parameter to 'remove' command") 307 | 308 | let version = params.commands[1].newVersion 309 | 310 | let isInstalled = isVersionInstalled(params, version) 311 | if not isInstalled: 312 | raise newException(ChooseNimError, 313 | "Version $1 is not installed." % $version) 314 | 315 | if version == getCurrentVersion(params): 316 | raise newException(ChooseNimError, 317 | "Cannot remove current version.") 318 | 319 | let extractDir = params.getInstallationDir(version) 320 | removeDir(extractDir) 321 | 322 | display("Info:", "Removed version " & $version, 323 | Success, HighPriority) 324 | 325 | 326 | proc performAction(params: CliParams) = 327 | # Report telemetry. 328 | report(initEvent(ActionEvent), params) 329 | 330 | case params.command.normalize 331 | of "update": 332 | update(params) 333 | of "show": 334 | show(params) 335 | of "versions": 336 | versions(params) 337 | of "remove": 338 | remove(params) 339 | else: 340 | choose(params) 341 | 342 | when isMainModule: 343 | var error = "" 344 | var hint = "" 345 | var params = newCliParams(proxyExeMode = false) 346 | try: 347 | parseCliParams(params) 348 | createDir(params.chooseNimDir) 349 | discard loadAnalytics(params) 350 | performAction(params) 351 | except NimbleError: 352 | let currentExc = (ref NimbleError)(getCurrentException()) 353 | (error, hint) = getOutputInfo(currentExc) 354 | # Report telemetry. 355 | report(currentExc, params) 356 | report(initEvent(ErrorEvent, label=currentExc.msg), params) 357 | 358 | if error.len > 0: 359 | displayTip() 360 | display("Error:", error, Error, HighPriority) 361 | if hint.len > 0: 362 | display("Hint:", hint, Warning, HighPriority) 363 | quit(QuitFailure) 364 | -------------------------------------------------------------------------------- /src/choosenim.nims: -------------------------------------------------------------------------------- 1 | when defined(macosx): 2 | switch("define", "curl") 3 | elif not defined(windows): 4 | switch("define", "curl") 5 | 6 | when defined(staticBuild): 7 | import "choosenimpkg/proxyexe.nims" 8 | -------------------------------------------------------------------------------- /src/choosenimpkg/builder.nim: -------------------------------------------------------------------------------- 1 | import os, times 2 | 3 | import nimblepkg/[version, cli] 4 | import nimblepkg/common as nimble_common 5 | 6 | import cliparams, download, utils, common, telemetry 7 | 8 | when defined(windows): 9 | import switcher 10 | 11 | proc buildFromCSources(params: CliParams) = 12 | when defined(windows): 13 | let arch = getGccArch(params) 14 | displayDebug("Detected", "arch as " & $arch & "bit") 15 | if arch == 32: 16 | doCmdRaw("build.bat") 17 | elif arch == 64: 18 | doCmdRaw("build64.bat") 19 | else: 20 | doCmdRaw("sh build.sh") 21 | 22 | proc buildCompiler(version: Version, params: CliParams) = 23 | ## Assumes that CWD contains the compiler (``build`` should have changed it). 24 | ## 25 | ## Assumes that binary hasn't already been built. 26 | let binDir = getCurrentDir() / "bin" 27 | if fileExists(getCurrentDir() / "build.sh"): 28 | buildFromCSources(params) 29 | else: 30 | display("Warning:", "Building from latest C sources. They may not be " & 31 | "compatible with the Nim version you have chosen to " & 32 | "install.", Warning, HighPriority) 33 | let path = downloadCSources(params) 34 | let extractDir = getCurrentDir() / "csources" 35 | extract(path, extractDir) 36 | 37 | display("Building", "C sources", priority = HighPriority) 38 | setCurrentDir(extractDir) # cd csources 39 | buildFromCSources(params) # sh build.sh 40 | setCurrentDir(extractDir.parentDir()) # cd .. 41 | 42 | when defined(windows): 43 | display("Building", "koch", priority = HighPriority) 44 | doCmdRaw("bin/nim.exe c koch") 45 | display("Building", "Nim", priority = HighPriority) 46 | doCmdRaw("koch.exe boot -d:release") 47 | else: 48 | display("Building", "koch", priority = HighPriority) 49 | doCmdRaw("./bin/nim c koch") 50 | display("Building", "Nim", priority = HighPriority) 51 | doCmdRaw("./koch boot -d:release") 52 | 53 | if not fileExists(binDir / "nim".addFileExt(ExeExt)): 54 | raise newException(ChooseNimError, "Nim binary is missing. Build failed.") 55 | 56 | proc buildTools(version: Version, params: CliParams) = 57 | ## Assumes that CWD contains the compiler. 58 | let binDir = getCurrentDir() / "bin" 59 | # TODO: I guess we should check for the other tools too? 60 | if fileExists(binDir / "nimble".addFileExt(ExeExt)): 61 | if not version.isDevel() or not params.latest: 62 | display("Tools:", "Already built", priority = HighPriority) 63 | return 64 | 65 | let msg = "tools (nimble, nimgrep, nimpretty, nimsuggest, testament)" 66 | display("Building", msg, priority = HighPriority) 67 | if fileExists(getCurrentDir() / "build.sh"): 68 | when defined(windows): 69 | doCmdRaw("bin/nim.exe c koch") 70 | doCmdRaw("koch.exe tools -d:release") 71 | else: 72 | doCmdRaw("./bin/nim c koch") 73 | doCmdRaw("./koch tools -d:release") 74 | else: 75 | when defined(windows): 76 | doCmdRaw("koch.exe tools -d:release") 77 | else: 78 | doCmdRaw("./koch tools -d:release") 79 | 80 | proc buildAll() = 81 | ## New method of building Nim. See https://github.com/dom96/choosenim/issues/256. 82 | ## 83 | ## This proc assumes that the extracted Nim sources contain a `build_all` 84 | ## script. 85 | ## 86 | ## Also assumes that CWD is set properly. 87 | when defined(windows): 88 | display("Building", "Nim using build_all.bat", priority = HighPriority) 89 | doCmdRaw("build_all.bat") 90 | else: 91 | display("Building", "Nim using build_all.sh", priority = HighPriority) 92 | doCmdRaw("sh build_all.sh") 93 | 94 | let binDir = getCurrentDir() / "bin" 95 | if not fileExists(binDir / "nim".addFileExt(ExeExt)): 96 | raise newException(ChooseNimError, "Nim binary is missing. Build failed.") 97 | 98 | proc setPermissions() = 99 | ## Assumes that CWD contains the compiler 100 | let binDir = getCurrentDir() / "bin" 101 | for kind, path in walkDir(binDir): 102 | if kind == pcFile: 103 | setFilePermissions(path, 104 | {fpUserRead, fpUserWrite, fpUserExec, 105 | fpGroupRead, fpGroupExec, 106 | fpOthersRead, fpOthersExec} 107 | ) 108 | display("Info", "Setting rwxr-xr-x permissions: " & path, Message, LowPriority) 109 | 110 | proc build*(extractDir: string, version: Version, params: CliParams) = 111 | # Report telemetry. 112 | report(initEvent(BuildEvent), params) 113 | let startTime = epochTime() 114 | 115 | let currentDir = getCurrentDir() 116 | setCurrentDir(extractDir) 117 | # Add MingW bin dir to PATH so that `build.bat` script can find gcc. 118 | let pathEnv = getEnv("PATH") 119 | when defined(windows): 120 | if not isDefaultCCInPath(params) and dirExists(params.getMingwBin()): 121 | putEnv("PATH", params.getMingwBin() & PathSep & pathEnv) 122 | defer: 123 | setCurrentDir(currentDir) 124 | putEnv("PATH", pathEnv) 125 | 126 | display("Building", "Nim " & $version, priority = HighPriority) 127 | 128 | var success = false 129 | try: 130 | if fileExists(getCurrentDir() / "bin" / "nim".addFileExt(ExeExt)): 131 | if not version.isDevel() or not params.latest: 132 | display("Compiler:", "Already built", priority = HighPriority) 133 | success = true 134 | return 135 | 136 | if ( 137 | fileExists(getCurrentDir() / "build_all.sh") and 138 | fileExists(getCurrentDir() / "build_all.bat") 139 | ): 140 | buildAll() 141 | else: 142 | buildCompiler(version, params) 143 | buildTools(version, params) 144 | success = true 145 | except NimbleError as exc: 146 | # Display error and output from build separately. 147 | let (error, hint) = getOutputInfo(exc) 148 | display("Exception:", error, Error, HighPriority) 149 | let newError = newException(ChooseNimError, "Build failed") 150 | newError.hint = hint 151 | raise newError 152 | finally: 153 | if success: 154 | # Ensure permissions are set correctly. 155 | setPermissions() 156 | 157 | # Delete c_code / csources 158 | try: 159 | removeDir(extractDir / "c_code") 160 | removeDir(extractDir / "csources") 161 | except Exception as exc: 162 | display("Warning:", "Cleaning c_code failed: " & exc.msg, Warning) 163 | 164 | # Report telemetry. 165 | report(initEvent(BuildSuccessEvent), params) 166 | report(initTiming(BuildTime, $version, startTime, $LabelSuccess), params) 167 | 168 | if not success and not params.skipClean: 169 | # Perform clean up. 170 | display("Cleaning", "failed build", priority = HighPriority) 171 | # TODO: Seems I cannot use a try inside a finally? 172 | # Getting `no exception to reraise` on the following line. 173 | try: 174 | removeDir(extractDir) 175 | except Exception as exc: 176 | display("Warning:", "Cleaning failed: " & exc.msg, Warning) 177 | 178 | # Report telemetry. 179 | report(initEvent(BuildFailureEvent), params) 180 | report(initTiming(BuildTime, $version, startTime, $LabelFailure), params) 181 | -------------------------------------------------------------------------------- /src/choosenimpkg/channel.nim: -------------------------------------------------------------------------------- 1 | ## This module implements information about release channels. 2 | ## 3 | ## In the future these may become configurable. 4 | 5 | import strutils, tables, os 6 | 7 | import nimblepkg/version 8 | 9 | import download, cliparams, switcher 10 | 11 | let 12 | channels = { 13 | "stable": "http://nim-lang.org/channels/stable", 14 | "devel": "#devel", 15 | "self": "https://nim-lang.org/choosenim/stable" 16 | }.toTable() 17 | 18 | proc isReleaseChannel*(command: string): bool = 19 | return command in channels 20 | 21 | proc getChannelVersion*(channel: string, params: CliParams, 22 | live=false): string = 23 | if not isReleaseChannel(channel): 24 | # Assume that channel is a version. 25 | return channel 26 | 27 | if not live: 28 | # Check for pinned version. 29 | let filename = params.getChannelsDir() / channel 30 | if fileExists(filename): 31 | return readFile(filename).strip() 32 | 33 | # Grab version from website or the hash table. 34 | let value = channels[channel] 35 | if value.startsWith("http"): 36 | # TODO: Better URL detection? 37 | return retrieveUrl(value).strip() 38 | else: 39 | return value 40 | 41 | proc pinChannelVersion*(channel: string, version: string, params: CliParams) = 42 | ## Assigns the specified version to the specified channel. This is done 43 | ## so that choosing ``stable`` won't install a new version (when it is 44 | ## released) until the ``update`` command is used. 45 | createDir(params.getChannelsDir()) 46 | 47 | writeFile(params.getChannelsDir() / channel, version) 48 | 49 | proc canUpdate*(version: Version, params: CliParams): bool = 50 | ## Determines whether this version can be updated. 51 | if version.isSpecial: 52 | return true 53 | 54 | return not isVersionInstalled(params, version) 55 | 56 | proc setCurrentChannel*(channel: string, params: CliParams) = 57 | writeFile(params.getCurrentChannelFile(), channel) 58 | 59 | proc getCurrentChannel*(params: CliParams): string = 60 | if not fileExists(params.getCurrentChannelFile()): 61 | return "" 62 | return readFile(params.getCurrentChannelFile()).strip() -------------------------------------------------------------------------------- /src/choosenimpkg/cliparams.nim: -------------------------------------------------------------------------------- 1 | import parseopt, strutils, os 2 | 3 | import nimblepkg/[cli, options, config] 4 | import nimblepkg/common as nimble_common 5 | import analytics 6 | 7 | import common 8 | 9 | type 10 | CliParams* = ref object 11 | commands*: seq[string] 12 | onlyInstalled*: bool 13 | choosenimDir*: string 14 | firstInstall*: bool 15 | nimbleOptions*: Options 16 | analytics*: PuppyAnalytics 17 | pendingReports*: int ## Count of pending telemetry reports. 18 | force*: bool 19 | latest*: bool 20 | skipClean*: bool 21 | 22 | let doc = """ 23 | choosenim: The Nim toolchain installer. 24 | 25 | Choose a job. Choose a mortgage. Choose life. Choose Nim. 26 | 27 | Usage: 28 | choosenim 29 | 30 | Example: 31 | choosenim 0.16.0 32 | Installs (if necessary) and selects version 0.16.0 of Nim. 33 | choosenim stable 34 | Installs (if necessary) Nim from the stable channel (latest stable release) 35 | and then selects it. 36 | choosenim devel [--latest] 37 | Installs (if necessary) and selects the most recent nightly build of Nim. 38 | The '--latest' flag selects and builds the latest commit in the devel branch 39 | choosenim ~/projects/nim 40 | Selects the specified Nim installation. 41 | choosenim update stable 42 | Updates the version installed on the stable release channel. 43 | choosenim update devel [--latest] 44 | Updates to the most recent nightly build of Nim. 45 | The '--latest' flag updates and builds the latest commit in the devel branch 46 | choosenim versions [--installed] 47 | Lists the available versions of Nim that choosenim has access to. 48 | choosenim remove 1.0.6 49 | Removes (if installed) version 1.0.6 of Nim. 50 | 51 | Channels: 52 | stable 53 | Describes the latest stable release of Nim. 54 | devel 55 | Describes the latest development (or nightly) release of Nim taken from 56 | the devel branch. 57 | 58 | Commands: 59 | update Installs the latest release of the specified 60 | version or channel. 61 | show Displays the selected version and channel. 62 | show path Prints only the path of the current Nim version. 63 | update self Updates choosenim itself. 64 | versions [--installed] Lists available versions of Nim, passing 65 | `--installed` only displays versions that 66 | are installed locally (no network requests). 67 | remove Removes specified version (if installed). 68 | 69 | Environment variables: 70 | GITHUB_TOKEN GitHub API Token. Some actions use the GitHub API. 71 | To avoid anonymous-access rate limits, supply a token 72 | generated at https://github.com/settings/tokens/new 73 | with the `public_repo` scope. 74 | 75 | Options: 76 | -h --help Show this output. 77 | -y --yes Agree to every question. 78 | --version Show version. 79 | --verbose Show low (and higher) priority output. 80 | --debug Show debug (and higher) priority output. 81 | --noColor Don't colorise output. 82 | 83 | --choosenimDir: Specify the directory where toolchains should be 84 | installed. Default: ~/.choosenim. 85 | --nimbleDir: Specify the Nimble directory where binaries will be 86 | placed. Default: ~/.nimble. 87 | --firstInstall Used by install script. 88 | --skipClean Skip cleaning of failed builds. 89 | """ 90 | 91 | proc command*(params: CliParams): string = 92 | return params.commands[0] 93 | 94 | proc getDownloadDir*(params: CliParams): string = 95 | return params.chooseNimDir / "downloads" 96 | 97 | proc getInstallDir*(params: CliParams): string = 98 | return params.chooseNimDir / "toolchains" 99 | 100 | proc getChannelsDir*(params: CliParams): string = 101 | return params.chooseNimDir / "channels" 102 | 103 | proc getBinDir*(params: CliParams): string = 104 | return params.nimbleOptions.getBinDir() 105 | 106 | proc getCurrentFile*(params: CliParams): string = 107 | ## Returns the path to the file which specifies the currently selected 108 | ## installation. The contents of this file is a path to the selected Nim 109 | ## directory. 110 | return params.chooseNimDir / "current" 111 | 112 | proc getCurrentChannelFile*(params: CliParams): string = 113 | return params.chooseNimDir / "current-channel" 114 | 115 | proc getAnalyticsFile*(params: CliParams): string = 116 | return params.chooseNimDir / "analytics" 117 | 118 | proc getCpuArch*(): int = 119 | ## Get CPU arch on Windows - get env var PROCESSOR_ARCHITECTURE 120 | var failMsg = "" 121 | 122 | let 123 | archEnv = getEnv("PROCESSOR_ARCHITECTURE") 124 | arch6432Env = getEnv("PROCESSOR_ARCHITEW6432") 125 | if arch6432Env.len != 0: 126 | # https://blog.differentpla.net/blog/2013/03/10/processor-architew6432/ 127 | result = 64 128 | elif "64" in archEnv: 129 | # https://superuser.com/a/1441469 130 | result = 64 131 | elif "86" in archEnv: 132 | result = 32 133 | else: 134 | failMsg = "PROCESSOR_ARCHITECTURE = " & archEnv & 135 | ", PROCESSOR_ARCHITEW6432 = " & arch6432Env 136 | 137 | # Die if unsupported - better fail than guess 138 | if result == 0: 139 | raise newException(ChooseNimError, 140 | "Could not detect CPU architecture: " & failMsg) 141 | 142 | proc getMingwPath*(params: CliParams): string = 143 | let arch = getCpuArch() 144 | return params.getInstallDir() / "mingw" & $arch 145 | 146 | proc getMingwBin*(params: CliParams): string = 147 | return getMingwPath(params) / "bin" 148 | 149 | proc getBinArchiveFormat*(): string = 150 | when defined(windows): 151 | return ".zip" 152 | else: 153 | return ".tar.xz" 154 | 155 | proc getDownloadPath*(params: CliParams, downloadUrl: string): string = 156 | let (_, name, ext) = downloadUrl.splitFile() 157 | return params.getDownloadDir() / name & ext 158 | 159 | proc writeHelp() = 160 | echo(doc) 161 | quit(QuitFailure) 162 | 163 | proc writeVersion() = 164 | echo("choosenim v$1 ($2 $3) [$4/$5]" % 165 | [chooseNimVersion, CompileDate, CompileTime, hostOS, hostCPU]) 166 | quit(QuitSuccess) 167 | 168 | proc writeNimbleBinDir(params: CliParams) = 169 | # Special option for scripts that install choosenim. 170 | echo(params.getBinDir()) 171 | quit(QuitSuccess) 172 | 173 | proc newCliParams*(proxyExeMode: bool): CliParams = 174 | new result 175 | result.commands = @[] 176 | result.choosenimDir = getHomeDir() / ".choosenim" 177 | # Init nimble params. 178 | try: 179 | result.nimbleOptions = initOptions() 180 | if not proxyExeMode: 181 | result.nimbleOptions.config = parseConfig() 182 | setNimbleDir(result.nimbleOptions) 183 | except NimbleQuit: 184 | discard 185 | 186 | proc parseCliParams*(params: var CliParams, proxyExeMode = false) = 187 | params = newCliParams(proxyExeMode) 188 | 189 | for kind, key, val in getopt(): 190 | case kind 191 | of cmdArgument: 192 | params.commands.add(key) 193 | of cmdLongOption, cmdShortOption: 194 | let normalised = key.normalize() 195 | # Don't want the proxyExe to return choosenim's help/version. 196 | case normalised 197 | of "help", "h": 198 | if not proxyExeMode: writeHelp() 199 | of "version", "v": 200 | if not proxyExeMode: writeVersion() 201 | of "getnimblebin": 202 | # Used by installer scripts to know where the choosenim executable 203 | # should be copied. 204 | if not proxyExeMode: writeNimbleBinDir(params) 205 | of "verbose": setVerbosity(LowPriority) 206 | of "debug": setVerbosity(DebugPriority) 207 | of "nocolor": setShowColor(false) 208 | of "choosenimdir": params.choosenimDir = val.absolutePath() 209 | of "nimbledir": params.nimbleOptions.nimbleDir = val.absolutePath() 210 | of "firstinstall": params.firstInstall = true 211 | of "y", "yes": params.nimbleOptions.forcePrompts = forcePromptYes 212 | of "installed": params.onlyInstalled = true 213 | of "force", "f": params.force = true 214 | of "latest", "l": params.latest = true 215 | of "skipclean": params.skipClean = true 216 | else: 217 | if not proxyExeMode: 218 | raise newException(ChooseNimError, "Unknown flag: --" & key) 219 | of cmdEnd: assert(false) 220 | 221 | if params.commands.len == 0 and not proxyExeMode: 222 | writeHelp() 223 | -------------------------------------------------------------------------------- /src/choosenimpkg/common.nim: -------------------------------------------------------------------------------- 1 | import nimblepkg/version 2 | 3 | type 4 | ChooseNimError* = object of NimbleError 5 | 6 | const 7 | chooseNimVersion* = "0.8.4" 8 | 9 | proxies* = [ 10 | "nim", 11 | "nimble", 12 | "nimgrep", 13 | "nimpretty", 14 | "nimsuggest", 15 | "testament", 16 | "nim-gdb", 17 | ] 18 | 19 | mingwProxies* = [ 20 | "gcc", 21 | "g++", 22 | "gdb", 23 | "ld" 24 | ] 25 | -------------------------------------------------------------------------------- /src/choosenimpkg/download.nim: -------------------------------------------------------------------------------- 1 | import httpclient, strutils, os, osproc, terminal, times, json, uri 2 | 3 | when defined(curl): 4 | import math 5 | 6 | import nimblepkg/[version, cli] 7 | when defined(curl): 8 | import libcurl except Version 9 | 10 | import cliparams, common, telemetry, utils 11 | 12 | const 13 | githubTagReleasesUrl = "https://api.github.com/repos/nim-lang/Nim/tags" 14 | githubNightliesReleasesUrl = "https://api.github.com/repos/nim-lang/nightlies/releases" 15 | githubUrl = "https://github.com/nim-lang/Nim" 16 | websiteUrlXz = "http://nim-lang.org/download/nim-$1.tar.xz" 17 | websiteUrlGz = "http://nim-lang.org/download/nim-$1.tar.gz" 18 | csourcesUrl = "https://github.com/nim-lang/csources" 19 | dlArchive = "archive/$1.tar.gz" 20 | binaryUrl = "http://nim-lang.org/download/nim-$1$2_x$3" & getBinArchiveFormat() 21 | userAgent = "choosenim/" & chooseNimVersion 22 | 23 | const # Windows-only 24 | mingwUrl = "http://nim-lang.org/download/mingw$1.zip" 25 | dllsUrl = "http://nim-lang.org/download/dlls.zip" 26 | 27 | const 28 | progressBarLength = 50 29 | 30 | proc showIndeterminateBar(progress, speed: BiggestInt, lastPos: var int) = 31 | try: 32 | eraseLine() 33 | except OSError: 34 | echo "" 35 | if lastPos >= progressBarLength: 36 | lastPos = 0 37 | 38 | var spaces = repeat(' ', progressBarLength) 39 | spaces[lastPos] = '#' 40 | lastPos.inc() 41 | stdout.write("[$1] $2mb $3kb/s" % [ 42 | spaces, $(progress div (1000*1000)), 43 | $(speed div 1000) 44 | ]) 45 | stdout.flushFile() 46 | 47 | proc showBar(fraction: float, speed: BiggestInt) = 48 | try: 49 | eraseLine() 50 | except OSError: 51 | echo "" 52 | let hashes = repeat('#', int(fraction * progressBarLength)) 53 | let spaces = repeat(' ', progressBarLength - hashes.len) 54 | stdout.write("[$1$2] $3% $4kb/s" % [ 55 | hashes, spaces, formatFloat(fraction * 100, precision=4), 56 | $(speed div 1000) 57 | ]) 58 | stdout.flushFile() 59 | 60 | proc addGithubAuthentication(url: string): string = 61 | let ghtoken = getEnv("GITHUB_TOKEN") 62 | if ghtoken == "": 63 | return url 64 | else: 65 | display("Info:", "Using the 'GITHUB_TOKEN' environment variable for GitHub API Token.", 66 | priority=HighPriority) 67 | return url.replace("https://api.github.com", "https://" & ghtoken & "@api.github.com") 68 | 69 | when defined(curl): 70 | proc checkCurl(code: Code) = 71 | if code != E_OK: 72 | raise newException(AssertionError, "CURL failed: " & $easy_strerror(code)) 73 | 74 | proc downloadFileCurl(url, outputPath: string) = 75 | displayDebug("Downloading using Curl") 76 | # Based on: https://curl.haxx.se/libcurl/c/url2file.html 77 | let curl = libcurl.easy_init() 78 | defer: 79 | curl.easy_cleanup() 80 | 81 | # Enable progress bar. 82 | #checkCurl curl.easy_setopt(OPT_VERBOSE, 1) 83 | checkCurl curl.easy_setopt(OPT_NOPROGRESS, 0) 84 | 85 | # Set which URL to download and tell curl to follow redirects. 86 | checkCurl curl.easy_setopt(OPT_URL, url) 87 | checkCurl curl.easy_setopt(OPT_FOLLOWLOCATION, 1) 88 | 89 | type 90 | UserData = ref object 91 | file: File 92 | lastProgressPos: int 93 | bytesWritten: int 94 | lastSpeedUpdate: float 95 | speed: BiggestInt 96 | needsUpdate: bool 97 | 98 | # Set up progress callback. 99 | proc onProgress(userData: pointer, dltotal, dlnow, ultotal, 100 | ulnow: float): cint = 101 | result = 0 # Ensure download isn't terminated. 102 | 103 | let userData = cast[UserData](userData) 104 | 105 | # Only update once per second. 106 | if userData.needsUpdate: 107 | userData.needsUpdate = false 108 | else: 109 | return 110 | 111 | let fraction = dlnow.float / dltotal.float 112 | if fraction.classify == fcNan: 113 | return 114 | 115 | if fraction == Inf: 116 | showIndeterminateBar(dlnow.BiggestInt, userData.speed, 117 | userData.lastProgressPos) 118 | else: 119 | showBar(fraction, userData.speed) 120 | 121 | checkCurl curl.easy_setopt(OPT_PROGRESSFUNCTION, onProgress) 122 | 123 | # Set up write callback. 124 | proc onWrite(data: ptr char, size: cint, nmemb: cint, 125 | userData: pointer): cint = 126 | let userData = cast[UserData](userData) 127 | let len = size * nmemb 128 | result = userData.file.writeBuffer(data, len).cint 129 | doAssert result == len 130 | 131 | # Handle speed measurement. 132 | const updateInterval = 0.25 133 | userData.bytesWritten += result 134 | if epochTime() - userData.lastSpeedUpdate > updateInterval: 135 | userData.speed = userData.bytesWritten * int(1/updateInterval) 136 | userData.bytesWritten = 0 137 | userData.lastSpeedUpdate = epochTime() 138 | userData.needsUpdate = true 139 | 140 | checkCurl curl.easy_setopt(OPT_WRITEFUNCTION, onWrite) 141 | 142 | # Open file for writing and set up UserData. 143 | let userData = UserData( 144 | file: open(outputPath, fmWrite), 145 | lastProgressPos: 0, 146 | lastSpeedUpdate: epochTime(), 147 | speed: 0 148 | ) 149 | defer: 150 | userData.file.close() 151 | checkCurl curl.easy_setopt(OPT_WRITEDATA, userData) 152 | checkCurl curl.easy_setopt(OPT_PROGRESSDATA, userData) 153 | 154 | # Download the file. 155 | checkCurl curl.easy_perform() 156 | 157 | # Verify the response code. 158 | var responseCode: int 159 | checkCurl curl.easy_getinfo(INFO_RESPONSE_CODE, addr responseCode) 160 | 161 | if responseCode != 200: 162 | raise newException(HTTPRequestError, 163 | "Expected HTTP code $1 got $2" % [$200, $responseCode]) 164 | 165 | proc downloadFileNim(url, outputPath: string) = 166 | displayDebug("Downloading using HttpClient") 167 | var client = newHttpClient(proxy = getProxy()) 168 | 169 | var lastProgressPos = 0 170 | proc onProgressChanged(total, progress, speed: BiggestInt) {.closure, gcsafe.} = 171 | let fraction = progress.float / total.float 172 | if fraction == Inf: 173 | showIndeterminateBar(progress, speed, lastProgressPos) 174 | else: 175 | showBar(fraction, speed) 176 | 177 | client.onProgressChanged = onProgressChanged 178 | 179 | client.downloadFile(url, outputPath) 180 | 181 | when defined(windows): 182 | import puppy 183 | proc downloadFilePuppy(url, outputPath: string) = 184 | displayDebug("Downloading using Puppy") 185 | let req = fetch(Request( 186 | url: parseUrl(url), 187 | verb: "get", 188 | headers: @[Header(key: "User-Agent", value: userAgent)] 189 | ) 190 | ) 191 | if req.code == 200: 192 | writeFile(outputPath, req.body) 193 | else: 194 | raise newException(HTTPRequestError, 195 | "Expected HTTP code $1 got $2" % [$200, $req.code]) 196 | 197 | proc downloadFile*(url, outputPath: string, params: CliParams) = 198 | # For debugging. 199 | display("GET:", url, priority = DebugPriority) 200 | 201 | # Telemetry 202 | let startTime = epochTime() 203 | 204 | # Create outputPath's directory if it doesn't exist already. 205 | createDir(outputPath.splitFile.dir) 206 | 207 | # Download to temporary file to prevent problems when choosenim crashes. 208 | let tempOutputPath = outputPath & "_temp" 209 | try: 210 | when defined(curl): 211 | downloadFileCurl(url, tempOutputPath) 212 | elif defined(windows): 213 | downloadFilePuppy(url, tempOutputPath) 214 | else: 215 | downloadFileNim(url, tempOutputPath) 216 | except HttpRequestError: 217 | echo("") # Skip line with progress bar. 218 | let msg = "Couldn't download file from $1.\nResponse was: $2" % 219 | [url, getCurrentExceptionMsg()] 220 | display("Info:", msg, Warning, MediumPriority) 221 | report(initTiming(DownloadTime, url, startTime, $LabelFailure), params) 222 | raise 223 | 224 | moveFile(tempOutputPath, outputPath) 225 | 226 | showBar(1, 0) 227 | echo("") 228 | 229 | report(initTiming(DownloadTime, url, startTime, $LabelSuccess), params) 230 | 231 | proc needsDownload(params: CliParams, downloadUrl: string, 232 | outputPath: var string): bool = 233 | ## Returns whether the download should commence. 234 | ## 235 | ## The `outputPath` argument is filled with the valid download path. 236 | result = true 237 | outputPath = params.getDownloadPath(downloadUrl) 238 | if outputPath.fileExists(): 239 | # TODO: Verify sha256. 240 | display("Info:", "$1 already downloaded" % outputPath, 241 | priority=HighPriority) 242 | return false 243 | 244 | proc retrieveUrl*(url: string): string 245 | proc downloadImpl(version: Version, params: CliParams): string = 246 | let arch = getGccArch(params) 247 | displayDebug("Detected", "arch as " & $arch & "bit") 248 | if version.isSpecial(): 249 | var reference, url = "" 250 | if $version in ["#devel", "#head"] and not params.latest: 251 | # Install nightlies by default for devel channel 252 | try: 253 | let rawContents = retrieveUrl(githubNightliesReleasesUrl.addGithubAuthentication()) 254 | let parsedContents = parseJson(rawContents) 255 | (url, reference) = getNightliesUrl(parsedContents, arch) 256 | if url.len == 0: 257 | display( 258 | "Warning", "Recent nightly release not found, installing latest devel commit.", 259 | Warning, HighPriority 260 | ) 261 | reference = if reference.len == 0: "devel" else: reference 262 | except HTTPRequestError: 263 | # Unable to get nightlies release json from github API, fallback 264 | # to `choosenim devel --latest` 265 | display("Warning", "Nightlies build unavailable, building latest commit", 266 | Warning, HighPriority) 267 | 268 | if url.len == 0: 269 | let 270 | commit = getLatestCommit(githubUrl, "devel") 271 | archive = if commit.len != 0: commit else: "devel" 272 | reference = 273 | case normalize($version) 274 | of "#head": 275 | archive 276 | else: 277 | ($version)[1 .. ^1] 278 | url = $(parseUri(githubUrl) / (dlArchive % reference)) 279 | display("Downloading", "Nim $1 from $2" % [reference, "GitHub"], 280 | priority = HighPriority) 281 | var outputPath: string 282 | if not needsDownload(params, url, outputPath): return outputPath 283 | 284 | downloadFile(url, outputPath, params) 285 | result = outputPath 286 | else: 287 | display("Downloading", "Nim $1 from $2" % [$version, "nim-lang.org"], 288 | priority = HighPriority) 289 | 290 | var outputPath: string 291 | 292 | # Use binary builds for Windows and Linux 293 | when defined(Windows) or defined(linux): 294 | let os = when defined(linux): "-linux" else: "" 295 | let binUrl = binaryUrl % [$version, os, $arch] 296 | if not needsDownload(params, binUrl, outputPath): return outputPath 297 | try: 298 | downloadFile(binUrl, outputPath, params) 299 | return outputPath 300 | except HttpRequestError: 301 | display("Info:", "Binary build unavailable, building from source", 302 | priority = HighPriority) 303 | 304 | let hasUnxz = findExe("unxz") != "" 305 | let url = (if hasUnxz: websiteUrlXz else: websiteUrlGz) % $version 306 | if not needsDownload(params, url, outputPath): return outputPath 307 | 308 | downloadFile(url, outputPath, params) 309 | result = outputPath 310 | 311 | proc download*(version: Version, params: CliParams): string = 312 | ## Returns the path of the downloaded .tar.(gz|xz) file. 313 | try: 314 | return downloadImpl(version, params) 315 | except HttpRequestError: 316 | raise newException(ChooseNimError, "Version $1 does not exist." % 317 | $version) 318 | 319 | proc downloadCSources*(params: CliParams): string = 320 | let 321 | commit = getLatestCommit(csourcesUrl, "master") 322 | archive = if commit.len != 0: commit else: "master" 323 | csourcesArchiveUrl = $(parseUri(csourcesUrl) / (dlArchive % archive)) 324 | 325 | var outputPath: string 326 | if not needsDownload(params, csourcesArchiveUrl, outputPath): 327 | return outputPath 328 | 329 | display("Downloading", "Nim C sources from GitHub", priority = HighPriority) 330 | downloadFile(csourcesArchiveUrl, outputPath, params) 331 | return outputPath 332 | 333 | proc downloadMingw*(params: CliParams): string = 334 | let 335 | arch = getCpuArch() 336 | url = mingwUrl % $arch 337 | var outputPath: string 338 | if not needsDownload(params, url, outputPath): 339 | return outputPath 340 | 341 | display("Downloading", "C compiler (Mingw$1)" % $arch, priority = HighPriority) 342 | downloadFile(url, outputPath, params) 343 | return outputPath 344 | 345 | proc downloadDLLs*(params: CliParams): string = 346 | var outputPath: string 347 | if not needsDownload(params, dllsUrl, outputPath): 348 | return outputPath 349 | 350 | display("Downloading", "DLLs (openssl, pcre, ...)", priority = HighPriority) 351 | downloadFile(dllsUrl, outputPath, params) 352 | return outputPath 353 | 354 | proc retrieveUrl*(url: string): string = 355 | when defined(curl): 356 | display("Curl", "Requesting " & url, priority = DebugPriority) 357 | # Based on: https://curl.haxx.se/libcurl/c/simple.html 358 | let curl = libcurl.easy_init() 359 | 360 | # Set which URL to retrieve and tell curl to follow redirects. 361 | checkCurl curl.easy_setopt(OPT_URL, url) 362 | checkCurl curl.easy_setopt(OPT_FOLLOWLOCATION, 1) 363 | 364 | var res = "" 365 | # Set up write callback. 366 | proc onWrite(data: ptr char, size: cint, nmemb: cint, 367 | userData: pointer): cint = 368 | var res = cast[ptr string](userData) 369 | var buffer = newString(size * nmemb) 370 | copyMem(addr buffer[0], data, buffer.len) 371 | res[].add(buffer) 372 | result = buffer.len.cint 373 | 374 | checkCurl curl.easy_setopt(OPT_WRITEFUNCTION, onWrite) 375 | checkCurl curl.easy_setopt(OPT_WRITEDATA, addr res) 376 | 377 | let usrAgentCopy = userAgent 378 | checkCurl curl.easy_setopt(OPT_USERAGENT, unsafeAddr usrAgentCopy[0]) 379 | 380 | # Download the file. 381 | checkCurl curl.easy_perform() 382 | 383 | # Verify the response code. 384 | var responseCode: int 385 | checkCurl curl.easy_getinfo(INFO_RESPONSE_CODE, addr responseCode) 386 | 387 | display("Curl", res, priority = DebugPriority) 388 | 389 | if responseCode != 200: 390 | raise newException(HTTPRequestError, 391 | "Expected HTTP code $1 got $2 for $3" % [$200, $responseCode, url]) 392 | 393 | return res 394 | elif defined(windows): 395 | return fetch( 396 | url, 397 | headers = @[Header(key: "User-Agent", value: userAgent)] 398 | ) 399 | else: 400 | display("Http", "Requesting " & url, priority = DebugPriority) 401 | var client = newHttpClient(proxy = getProxy(), userAgent = userAgent) 402 | return client.getContent(url) 403 | 404 | proc getOfficialReleases*(params: CliParams): seq[Version] = 405 | let rawContents = retrieveUrl(githubTagReleasesUrl.addGithubAuthentication()) 406 | let parsedContents = parseJson(rawContents) 407 | let cutOffVersion = newVersion("0.16.0") 408 | 409 | var releases: seq[Version] = @[] 410 | for release in parsedContents: 411 | let name = release["name"].getStr().strip(true, false, {'v'}) 412 | let version = name.newVersion 413 | if cutOffVersion <= version: 414 | releases.add(version) 415 | return releases 416 | 417 | template isDevel*(version: Version): bool = 418 | $version in ["#head", "#devel"] 419 | 420 | proc gitUpdate*(version: Version, extractDir: string, params: CliParams): bool = 421 | if version.isDevel() and params.latest: 422 | let git = findExe("git") 423 | if git.len != 0 and fileExists(extractDir / ".git" / "config"): 424 | result = true 425 | 426 | let lastDir = getCurrentDir() 427 | setCurrentDir(extractDir) 428 | defer: 429 | setCurrentDir(lastDir) 430 | 431 | display("Fetching", "latest changes", priority = HighPriority) 432 | for cmd in [" fetch --all", " reset --hard origin/devel"]: 433 | var (outp, errC) = execCmdEx(git.quoteShell & cmd) 434 | if errC != QuitSuccess: 435 | display("Warning:", "git" & cmd & " failed: " & outp, Warning, priority = HighPriority) 436 | return false 437 | 438 | proc gitInit*(version: Version, extractDir: string, params: CliParams) = 439 | createDir(extractDir / ".git") 440 | if version.isDevel(): 441 | let git = findExe("git") 442 | if git.len != 0: 443 | let lastDir = getCurrentDir() 444 | setCurrentDir(extractDir) 445 | defer: 446 | setCurrentDir(lastDir) 447 | 448 | var init = true 449 | display("Setting", "up git repository", priority = HighPriority) 450 | for cmd in [" init", " remote add origin https://github.com/nim-lang/nim"]: 451 | var (outp, errC) = execCmdEx(git.quoteShell & cmd) 452 | if errC != QuitSuccess: 453 | display("Warning:", "git" & cmd & " failed: " & outp, Warning, priority = HighPriority) 454 | init = false 455 | break 456 | 457 | if init: 458 | discard gitUpdate(version, extractDir, params) 459 | 460 | when isMainModule: 461 | 462 | echo retrieveUrl("https://nim-lang.org") 463 | -------------------------------------------------------------------------------- /src/choosenimpkg/env.nim: -------------------------------------------------------------------------------- 1 | import os, strutils 2 | 3 | import nimblepkg/[cli, options] 4 | 5 | import cliparams 6 | 7 | when defined(windows): 8 | # From finish.nim in nim-lang/Nim/tools 9 | import registry 10 | 11 | proc tryGetUnicodeValue(path, key: string, handle: HKEY): string = 12 | # Get a unicode value from the registry or "" 13 | try: 14 | result = getUnicodeValue(path, key, handle) 15 | except: 16 | result = "" 17 | 18 | proc addToPathEnv(path: string) = 19 | # Append path to user PATH to registry 20 | var paths = tryGetUnicodeValue(r"Environment", "Path", HKEY_CURRENT_USER) 21 | let path = if path.contains(Whitespace): "\"" & path & "\"" else: path 22 | if paths.len > 0: 23 | if paths[^1] != PathSep: 24 | paths.add PathSep 25 | paths.add path 26 | else: 27 | paths = path 28 | setUnicodeValue(r"Environment", "Path", paths, HKEY_CURRENT_USER) 29 | 30 | proc setNimbleBinPath*(params: CliParams) = 31 | # Ask the user and add nimble bin to PATH 32 | let nimbleDesiredPath = params.getBinDir() 33 | if prompt(params.nimbleOptions.forcePrompts, 34 | nimbleDesiredPath & " is not in your PATH environment variable.\n" & 35 | " Should it be added permanently?"): 36 | addToPathEnv(nimbleDesiredPath) 37 | display("Note:", "PATH changes will only take effect in new sessions.", 38 | priority = HighPriority) 39 | 40 | proc isNimbleBinInPath*(params: CliParams): bool = 41 | # This proc searches the $PATH variable for the nimble bin directory, 42 | # typically ~/.nimble/bin 43 | result = false 44 | let nimbleDesiredPath = params.getBinDir() 45 | when defined(windows): 46 | # Getting PATH from registry since it is the ultimate source of 47 | # truth and session local $PATH can be changed. 48 | let paths = tryGetUnicodeValue(r"Environment", "Path", 49 | HKEY_CURRENT_USER) & PathSep & tryGetUnicodeValue( 50 | r"System\CurrentControlSet\Control\Session Manager\Environment", "Path", 51 | HKEY_LOCAL_MACHINE) 52 | else: 53 | let paths = getEnv("PATH") 54 | for path in paths.split(PathSep): 55 | if path.len == 0: continue 56 | let path = path.strip(chars = {'"'}) 57 | let expandedPath = 58 | try: 59 | expandFilename(path) 60 | except: 61 | "" 62 | if expandedPath.cmpIgnoreCase(nimbleDesiredPath) == 0: 63 | result = true 64 | break -------------------------------------------------------------------------------- /src/choosenimpkg/proxyexe.nim: -------------------------------------------------------------------------------- 1 | # This file is embedded in the `choosenim` executable and is written to 2 | # ~/.nimble/bin/. It emulates a portable symlink with some nice additional 3 | # features. 4 | 5 | import strutils, os, osproc 6 | 7 | import nimblepkg/[cli, options, version] 8 | import nimblepkg/common as nimbleCommon 9 | import cliparams 10 | from common import ChooseNimError, mingwProxies 11 | 12 | proc getSelectedPath(params: CliParams): string = 13 | var path = "" 14 | try: 15 | path = params.getCurrentFile() 16 | if not fileExists(path): 17 | let msg = "No installation has been chosen. (File missing: $1)" % path 18 | raise newException(ChooseNimError, msg) 19 | 20 | result = readFile(path) 21 | except Exception as exc: 22 | let msg = "Unable to read $1. (Error was: $2)" % [path, exc.msg] 23 | raise newException(ChooseNimError, msg) 24 | 25 | proc getExePath(params: CliParams): string 26 | {.raises: [ChooseNimError, ValueError].} = 27 | try: 28 | let exe = getAppFilename().extractFilename 29 | let exeName = exe.splitFile.name 30 | 31 | if exeName in mingwProxies and defined(windows): 32 | return getMingwBin(params) / exe 33 | else: 34 | return getSelectedPath(params) / "bin" / exe 35 | except Exception as exc: 36 | let msg = "getAppFilename failed. (Error was: $1)" % exc.msg 37 | raise newException(ChooseNimError, msg) 38 | 39 | proc main(params: CliParams) {.raises: [ChooseNimError, ValueError].} = 40 | let exePath = getExePath(params) 41 | if not fileExists(exePath): 42 | raise newException(ChooseNimError, 43 | "Requested executable is missing. (Path: $1)" % exePath) 44 | 45 | try: 46 | # Launch the desired process. 47 | let p = startProcess(exePath, args=commandLineParams(), 48 | options={poParentStreams}) 49 | let exitCode = p.waitForExit() 50 | p.close() 51 | quit(exitCode) 52 | except Exception as exc: 53 | raise newException(ChooseNimError, 54 | "Spawning of process failed. (Error was: $1)" % exc.msg) 55 | 56 | when isMainModule: 57 | var error = "" 58 | var hint = "" 59 | var params = newCliParams(proxyExeMode = true) 60 | try: 61 | parseCliParams(params, proxyExeMode = true) 62 | main(params) 63 | except NimbleError as exc: 64 | (error, hint) = getOutputInfo(exc) 65 | 66 | if error.len > 0: 67 | displayTip() 68 | display("Error:", error, Error, HighPriority) 69 | if hint.len > 0: 70 | display("Hint:", hint, Warning, HighPriority) 71 | 72 | display("Info:", "If unexpected, please report this error to " & 73 | "https://github.com/dom96/choosenim", Warning, HighPriority) 74 | quit(1) 75 | -------------------------------------------------------------------------------- /src/choosenimpkg/proxyexe.nim.cfg: -------------------------------------------------------------------------------- 1 | -d:useFork -------------------------------------------------------------------------------- /src/choosenimpkg/proxyexe.nims: -------------------------------------------------------------------------------- 1 | when defined(staticBuild): 2 | when defined(linux): 3 | putEnv("CC", "musl-gcc") 4 | switch("gcc.exe", "musl-gcc") 5 | switch("gcc.linkerexe", "musl-gcc") 6 | when not defined(OSX): 7 | switch("passL", "-static") 8 | -------------------------------------------------------------------------------- /src/choosenimpkg/switcher.nim: -------------------------------------------------------------------------------- 1 | import os, strutils, osproc, pegs 2 | 3 | import nimblepkg/[cli, version, options] 4 | from nimblepkg/packageinfo import getNameVersion 5 | 6 | import cliparams, common 7 | 8 | when defined(windows): 9 | import env 10 | 11 | proc compileProxyexe() = 12 | var cmd = 13 | when defined(windows): 14 | "cmd /C \"cd ../../ && nimble c" 15 | else: 16 | "cd ../../ && nimble c" 17 | when defined(release): 18 | cmd.add " -d:release" 19 | when defined(staticBuild): 20 | cmd.add " -d:staticBuild" 21 | cmd.add " src/choosenimpkg/proxyexe" 22 | when defined(windows): 23 | cmd.add("\"") 24 | let (output, exitCode) = gorgeEx(cmd) 25 | doAssert exitCode == 0, $(output, cmd) 26 | 27 | static: compileProxyexe() 28 | 29 | const 30 | proxyExe = staticRead("proxyexe".addFileExt(ExeExt)) 31 | 32 | proc getInstallationDir*(params: CliParams, version: Version): string = 33 | return params.getInstallDir() / ("nim-$1" % $version) 34 | 35 | proc isVersionInstalled*(params: CliParams, version: Version): bool = 36 | return fileExists(params.getInstallationDir(version) / "bin" / 37 | "nim".addFileExt(ExeExt)) 38 | 39 | proc getSelectedPath*(params: CliParams): string = 40 | if fileExists(params.getCurrentFile()): readFile(params.getCurrentFile()) 41 | else: "" 42 | 43 | proc getProxyPath(params: CliParams, bin: string): string = 44 | return params.getBinDir() / bin.addFileExt(ExeExt) 45 | 46 | proc areProxiesInstalled(params: CliParams, proxies: openarray[string]): bool = 47 | result = true 48 | for proxy in proxies: 49 | # Verify that proxy exists. 50 | let path = params.getProxyPath(proxy) 51 | if not fileExists(path): 52 | return false 53 | 54 | # Verify that proxy binary is up-to-date. 55 | let contents = readFile(path) 56 | if contents != proxyExe: 57 | return false 58 | 59 | proc isDefaultCCInPath*(params: CliParams): bool = 60 | # Fixes issue #104 61 | when defined(macosx): 62 | return findExe("clang") != "" 63 | else: 64 | return findExe("gcc") != "" 65 | 66 | proc needsCCInstall*(params: CliParams): bool = 67 | ## Determines whether the system needs a C compiler to be installed. 68 | let inPath = isDefaultCCInPath(params) 69 | 70 | when defined(windows): 71 | let inMingwDir = 72 | when defined(windows): 73 | fileExists(params.getMingwBin() / "gcc".addFileExt(ExeExt)) 74 | else: false 75 | 76 | # Check whether the `gcc` we have in PATH is actually choosenim's proxy exe. 77 | # If so and toolchain mingw dir doesn't exit then we need to install. 78 | if inPath and findExe("gcc") == params.getProxyPath("gcc"): 79 | return not inMingwDir 80 | 81 | return not inPath 82 | 83 | proc needsDLLInstall*(params: CliParams): bool = 84 | ## Determines whether DLLs need to be installed (Windows-only). 85 | ## 86 | ## TODO: In the future we can probably extend this and let the user 87 | ## know what DLLs they are missing on all operating systems. 88 | proc isInstalled(params: CliParams, name: string): bool = 89 | let 90 | inPath = findExe(name, extensions=["dll"]) != "" 91 | inNimbleBin = fileExists(params.getBinDir() / name & ".dll") 92 | 93 | return inPath or inNimbleBin 94 | 95 | for dll in ["libeay", "pcre", "pdcurses", "sqlite3_", "ssleay"]: 96 | for bit in ["32", "64"]: 97 | result = not isInstalled(params, dll & bit) 98 | if result: return 99 | 100 | proc getNimbleVersion(toolchainPath: string): Version = 101 | result = newVersion("0.8.6") # We assume that everything is fine. 102 | let command = toolchainPath / "bin" / "nimble".addFileExt(ExeExt) 103 | let (output, _) = execCmdEx(command & " -v") 104 | var matches: array[0 .. MaxSubpatterns, string] 105 | if output.find(peg"'nimble v'{(\d+\.)+\d+}", matches) != -1: 106 | result = newVersion(matches[0]) 107 | else: 108 | display("Warning:", "Could not find toolchain's Nimble version.", 109 | Warning, MediumPriority) 110 | 111 | proc writeProxy(bin: string, params: CliParams) = 112 | # Create the ~/.nimble/bin dir in case it doesn't exist. 113 | createDir(params.getBinDir()) 114 | 115 | let proxyPath = params.getProxyPath(bin) 116 | 117 | if bin == "nimble": 118 | # Check for "lib" dir in ~/.nimble. Issue #13. 119 | let dir = params.nimbleOptions.getNimbleDir() / "lib" 120 | if dirExists(dir): 121 | let msg = ("Nimble will fail because '$1' exists. Would you like me " & 122 | "to remove it?") % dir 123 | if prompt(dontForcePrompt, msg): 124 | removeDir(dir) 125 | display("Removed", dir, priority = HighPriority) 126 | 127 | if symlinkExists(proxyPath): 128 | let msg = "Symlink for '$1' detected in '$2'. Can I remove it?" % 129 | [bin, proxyPath.splitFile().dir] 130 | if not prompt(dontForcePrompt, msg): return 131 | let symlinkPath = expandSymlink(proxyPath) 132 | removeFile(proxyPath) 133 | display("Removed", "symlink pointing to $1" % symlinkPath, 134 | priority = HighPriority) 135 | 136 | # Don't write the file again if it already exists. 137 | if fileExists(proxyPath) and readFile(proxyPath) == proxyExe: return 138 | 139 | try: 140 | writeFile(proxyPath, proxyExe) 141 | except IOError: 142 | display("Warning:", "component '$1' possibly in use, write failed" % bin, Warning, 143 | priority = HighPriority) 144 | return 145 | 146 | # Make sure the exe has +x flag. 147 | setFilePermissions(proxyPath, 148 | getFilePermissions(proxyPath) + {fpUserExec}) 149 | display("Installed", "component '$1'" % bin, priority = HighPriority) 150 | 151 | # Check whether this is in the user's PATH. 152 | let fromPATH = findExe(bin) 153 | display("Debug:", "Proxy path: " & proxyPath, priority = DebugPriority) 154 | display("Debug:", "findExe: " & fromPATH, priority = DebugPriority) 155 | if fromPATH == "" and not params.firstInstall: 156 | let msg = 157 | when defined(windows): 158 | "Binary '$1' isn't in your PATH" % bin 159 | else: 160 | "Binary '$1' isn't in your PATH. Ensure that '$2' is in your PATH." % 161 | [bin, params.getBinDir()] 162 | display("Hint:", msg, Warning, HighPriority) 163 | elif fromPATH != "" and fromPATH != proxyPath: 164 | display("Warning:", "Binary '$1' is shadowed by '$2'." % 165 | [bin, fromPATH], Warning, HighPriority) 166 | display("Hint:", "Ensure that '$1' is before '$2' in the PATH env var." % 167 | [params.getBinDir(), fromPATH.splitFile.dir], Warning, HighPriority) 168 | 169 | proc switchToPath(filepath: string, params: CliParams): bool = 170 | ## Switches to the specified file path that should point to the root of 171 | ## the Nim repo. 172 | ## 173 | ## Returns `false` when no switching occurs (because that version was 174 | ## already selected). 175 | result = true 176 | if not fileExists(filepath / "bin" / "nim".addFileExt(ExeExt)): 177 | let msg = "No 'nim' binary found in '$1'." % filepath / "bin" 178 | raise newException(ChooseNimError, msg) 179 | 180 | # Check Nimble version to give a warning when it's too old. 181 | let nimbleVersion = getNimbleVersion(filepath) 182 | if nimbleVersion < newVersion("0.8.6"): 183 | display("Warning:", ("Nimble v$1 is not supported by choosenim, using it " & 184 | "will yield errors.") % $nimbleVersion, 185 | Warning, HighPriority) 186 | display("Hint:", "Installing Nim from GitHub will ensure that a working " & 187 | "version of Nimble is installed. You can do so by " & 188 | "running `choosenim \"#v0.16.0\"` or similar.", 189 | Warning, HighPriority) 190 | 191 | var proxiesToInstall = @proxies 192 | # Handle MingW proxies. 193 | when defined(windows): 194 | if not isDefaultCCInPath(params): 195 | let mingwBin = getMingwBin(params) 196 | if not fileExists(mingwBin / "gcc".addFileExt(ExeExt)): 197 | let msg = "No 'gcc' binary found in '$1'." % mingwBin 198 | raise newException(ChooseNimError, msg) 199 | 200 | proxiesToInstall.add(mingwProxies) 201 | 202 | # Return early if this version is already selected. 203 | let selectedPath = params.getSelectedPath() 204 | let proxiesInstalled = params.areProxiesInstalled(proxiesToInstall) 205 | if selectedPath == filepath and proxiesInstalled: 206 | return false 207 | else: 208 | # Write selected path to "current file". 209 | writeFile(params.getCurrentFile(), filepath) 210 | 211 | # Create the proxy executables. 212 | for proxy in proxiesToInstall: 213 | writeProxy(proxy, params) 214 | 215 | when defined(windows): 216 | if not isNimbleBinInPath(params): 217 | display("Hint:", "Use 'choosenim --firstInstall' to add\n" & 218 | "$1 to your PATH." % params.getBinDir(), Warning, HighPriority) 219 | 220 | proc switchTo*(version: Version, params: CliParams) = 221 | ## Switches to the specified version by writing the appropriate proxy 222 | ## into $nimbleDir/bin. 223 | assert params.isVersionInstalled(version), 224 | "Cannot switch to non-installed version" 225 | 226 | if switchToPath(params.getInstallationDir(version), params): 227 | display("Switched", "to Nim " & $version, Success, HighPriority) 228 | else: 229 | display("Info:", "Version $1 already selected" % $version, 230 | priority = HighPriority) 231 | 232 | proc switchTo*(filepath: string, params: CliParams) = 233 | ## Switches to an existing Nim installation. 234 | let filepath = expandFilename(filepath) 235 | if switchToPath(filepath, params): 236 | display("Switched", "to Nim ($1)" % filepath, Success, HighPriority) 237 | else: 238 | display("Info:", "Path '$1' already selected" % filepath, 239 | priority = HighPriority) 240 | 241 | proc getSelectedVersion*(params: CliParams): Version = 242 | let path = getSelectedPath(params) 243 | let (_, version) = getNameVersion(path) 244 | return version.newVersion 245 | -------------------------------------------------------------------------------- /src/choosenimpkg/telemetry.nim: -------------------------------------------------------------------------------- 1 | # Copyright (C) Dominik Picheta. All rights reserved. 2 | # BSD-3-Clause License. Look at license.txt for more info. 3 | 4 | import os, strutils, options, times, asyncdispatch 5 | 6 | import analytics, nimblepkg/cli 7 | 8 | when defined(windows): 9 | import osinfo/win 10 | else: 11 | import osinfo/posix 12 | 13 | import cliparams, common, utils 14 | 15 | type 16 | EventCategory* = enum 17 | ActionEvent, 18 | BuildEvent, BuildSuccessEvent, BuildFailureEvent, 19 | ErrorEvent, 20 | OSInfoEvent 21 | 22 | Event* = object 23 | category*: EventCategory 24 | action*: string 25 | label*: string 26 | value*: Option[int] 27 | 28 | TimingCategory* = enum 29 | BuildTime, 30 | DownloadTime 31 | 32 | Timing* = object 33 | category*: TimingCategory 34 | name*: string 35 | time*: int 36 | label*: string 37 | 38 | LabelCategory* = enum 39 | LabelSuccess, LabelFailure 40 | 41 | 42 | 43 | proc initEvent*(category: EventCategory, action="", label="", 44 | value=none(int)): Event = 45 | let cmd = "choosenim " & commandLineParams().join(" ") 46 | return Event(category: category, 47 | action: if action.len == 0: cmd else: action, 48 | label: label, value: value) 49 | 50 | proc initTiming*(category: TimingCategory, name: string, startTime: float, 51 | label=""): Timing = 52 | ## The `startTime` is the Unix epoch timestamp for when the timing started 53 | ## (from `epochTime`). 54 | ## This function will automatically calculate the elapsed time based on that. 55 | let elapsed = int((epochTime() - startTime)*1000) 56 | return Timing(category: category, 57 | name: name, 58 | label: label, time: elapsed) 59 | 60 | proc promptCustom(msg: string, params: CliParams): string = 61 | if params.nimbleOptions.forcePrompts == forcePromptYes: 62 | display("Prompt:", msg, Warning, HighPriority) 63 | display("Answer:", "Forced Yes", Warning, HighPriority) 64 | return "y" 65 | else: 66 | return promptCustom(msg, "") 67 | 68 | proc analyticsPrompt(params: CliParams) = 69 | let msg = ("Can choosenim record and send anonymised telemetry " & 70 | "data? [y/n]\n" & 71 | "Anonymous aggregate user analytics allow us to prioritise\n" & 72 | "fixes and features based on how, where and when people " & 73 | "use Nim.\n" & 74 | "For more details see: https://goo.gl/NzUEPf.") 75 | 76 | let resp = promptCustom(msg, params) 77 | let analyticsFile = params.getAnalyticsFile() 78 | case resp.normalize 79 | of "y", "yes": 80 | let clientID = analytics.genClientID() 81 | writeFile(analyticsFile, clientID) 82 | display("Info:", "Your client ID is " & clientID, priority=LowPriority) 83 | of "n", "no": 84 | # Write an empty file to signify that the user answered "No". 85 | writeFile(analyticsFile, "") 86 | return 87 | else: 88 | # Force the user to answer. 89 | analyticsPrompt(params) 90 | 91 | proc report*(obj: Event | Timing | ref Exception, params: CliParams) 92 | proc loadAnalytics*(params: CliParams): bool = 93 | ## Returns ``true`` if ``analytics`` object has been loaded successfully. 94 | if getEnv("CHOOSENIM_NO_ANALYTICS") == "1" or getEnv("DO_NOT_TRACK") == "1": 95 | display("Info:", 96 | "Not sending analytics because either CHOOSENIM_NO_ANALYTICS or DO_NOT_TRACK is set.", 97 | priority=MediumPriority) 98 | return false 99 | 100 | if params.isNil: 101 | raise newException(ValueError, "Params is nil.") 102 | 103 | if not params.analytics.isNil: 104 | return true 105 | 106 | let analyticsFile = params.getAnalyticsFile() 107 | var prompted = false 108 | if not fileExists(analyticsFile): 109 | params.analyticsPrompt() 110 | prompted = true 111 | 112 | let clientID = readFile(analyticsFile) 113 | if clientID.len == 0: 114 | display("Info:", 115 | "No client ID found in '$1', not sending analytics." % 116 | analyticsFile, 117 | priority=LowPriority) 118 | return false 119 | 120 | params.analytics = newPuppyAnalytics("UA-105812497-1", clientID, "choosenim", 121 | chooseNimVersion, proxy = getProxy(), 122 | timeout=5) 123 | 124 | # Report OS info only once. 125 | if prompted: 126 | when defined(windows): 127 | let systemVersion = $getVersionInfo() 128 | else: 129 | let systemVersion = getSystemVersion() 130 | report(initEvent(OSInfoEvent, systemVersion), params) 131 | 132 | return true 133 | 134 | proc reportAsyncError(fut: Future[void], params: CliParams) = 135 | fut.callback = 136 | proc (fut: Future[void]) {.gcsafe.} = 137 | {.gcsafe.}: 138 | if fut.failed: 139 | display("Warning: ", "Could not report analytics due to error: " & 140 | fut.error.msg, Warning, MediumPriority) 141 | params.pendingReports.dec() 142 | 143 | proc hasPendingReports*(params: CliParams): bool = params.pendingReports > 0 144 | 145 | 146 | proc report*(obj: Event | Timing | ref Exception, params: CliParams) = 147 | try: 148 | if not loadAnalytics(params): 149 | return 150 | except Exception as exc: 151 | display("Warning:", "Could not load analytics reporter due to error:" & 152 | exc.msg, Warning, MediumPriority) 153 | return 154 | 155 | displayDebug("Reporting to analytics...") 156 | 157 | try: 158 | when obj is Event: 159 | params.analytics.reportEvent($obj.category, obj.action, 160 | obj.label, obj.value) 161 | elif obj is Timing: 162 | params.analytics.reportTiming($obj.category, obj.name, 163 | obj.time, obj.label) 164 | else: 165 | params.analytics.reportException(obj.msg) 166 | 167 | except Exception as exc: 168 | display("Warning:", "Could not report to analytics due to error:" & 169 | exc.msg, Warning, MediumPriority) 170 | 171 | -------------------------------------------------------------------------------- /src/choosenimpkg/utils.nim: -------------------------------------------------------------------------------- 1 | import httpclient, json, os, strutils, osproc, uri, sequtils 2 | 3 | import nimblepkg/[cli, version] 4 | import zippy/tarballs as zippy_tarballs 5 | import zippy/ziparchives as zippy_zips 6 | 7 | import cliparams, common 8 | 9 | when defined(windows): 10 | import switcher 11 | 12 | proc parseVersion*(versionStr: string): Version = 13 | if versionStr[0] notin {'#', '\0'} + Digits: 14 | let msg = "Invalid version, path or unknown channel.\n" & 15 | "Try 1.0.6, #head, #commitHash, or stable.\n" & 16 | "For example: choosenim #head.\n \n"& 17 | "See --help for more examples." 18 | raise newException(ChooseNimError, msg) 19 | 20 | let parts = versionStr.split(".") 21 | if parts.len >= 3 and parts[2].parseInt() mod 2 != 0: 22 | let msg = ("Version $# is a development version of Nim. This means " & 23 | "it hasn't been released so you cannot install it this " & 24 | "way. All unreleased versions of Nim " & 25 | "have an odd patch number in their version.") % versionStr 26 | let exc = newException(ChooseNimError, msg) 27 | exc.hint = "If you want to install the development version then run " & 28 | "`choosenim devel`." 29 | raise exc 30 | 31 | result = newVersion(versionStr) 32 | 33 | proc doCmdRaw*(cmd: string) = 34 | # To keep output in sequence 35 | stdout.flushFile() 36 | stderr.flushFile() 37 | 38 | displayDebug("Executing", cmd) 39 | displayDebug("Work Dir", getCurrentDir()) 40 | let (output, exitCode) = execCmdEx(cmd) 41 | displayDebug("Finished", "with exit code " & $exitCode) 42 | displayDebug("Output", output) 43 | 44 | if exitCode != QuitSuccess: 45 | raise newException(ChooseNimError, 46 | "Execution failed with exit code $1\nCommand: $2\nOutput: $3" % 47 | [$exitCode, cmd, output]) 48 | 49 | proc extract*(path: string, extractDir: string) = 50 | display("Extracting", path.extractFilename(), priority = HighPriority) 51 | 52 | if path.splitFile().ext == ".xz": 53 | when defined(windows): 54 | # We don't ship with `unxz` on Windows, instead assume that we get 55 | # a .zip on this platform. 56 | raise newException( 57 | ChooseNimError, "Unable to extract. Tar.xz files are not supported on Windows." 58 | ) 59 | else: 60 | let tarFile = path.changeFileExt("") 61 | removeFile(tarFile) # just in case it exists, if it does `unxz` fails. 62 | if findExe("unxz") == "": 63 | raise newException( 64 | ChooseNimError, "Unable to extract. Need `unxz` to extract .tar.xz file. See https://github.com/dom96/choosenim/issues/290." 65 | ) 66 | doCmdRaw("unxz " & quoteShell(path)) 67 | extract(tarFile, extractDir) # We remove the .xz extension 68 | return 69 | 70 | let tempDir = getTempDir() / "choosenim-extraction" 71 | removeDir(tempDir) 72 | 73 | try: 74 | case path.splitFile.ext 75 | of ".zip": 76 | zippy_zips.extractAll(path, tempDir) 77 | of ".tar", ".gz": 78 | if findExe("tar") != "": 79 | # TODO: Workaround for high mem usage of zippy (https://github.com/guzba/zippy/issues/31). 80 | createDir(tempDir) 81 | doCmdRaw("tar xf " & quoteShell(path) & " -C " & quoteShell(tempDir)) 82 | else: 83 | zippy_tarballs.extractAll(path, tempDir) 84 | else: 85 | raise newException( 86 | ValueError, "Unsupported format for extraction: " & path 87 | ) 88 | except Exception as exc: 89 | raise newException(ChooseNimError, "Unable to extract. Error was '$1'." % 90 | exc.msg) 91 | 92 | # Skip outer directory. 93 | # Same as: https://github.com/dom96/untar/blob/d21f7229b/src/untar.nim 94 | # 95 | # Determine which directory to copy. 96 | var srcDir = tempDir 97 | let contents = toSeq(walkDir(srcDir)) 98 | if contents.len == 1: 99 | # Skip the outer directory. 100 | srcDir = contents[0][1] 101 | 102 | # Finally copy the directory to what the user specified. 103 | copyDir(srcDir, extractDir) 104 | 105 | proc getProxy*(): Proxy = 106 | ## Returns ``nil`` if no proxy is specified. 107 | var url = "" 108 | try: 109 | if existsEnv("http_proxy"): 110 | url = getEnv("http_proxy") 111 | elif existsEnv("https_proxy"): 112 | url = getEnv("https_proxy") 113 | except ValueError: 114 | display("Warning:", "Unable to parse proxy from environment: " & 115 | getCurrentExceptionMsg(), Warning, HighPriority) 116 | 117 | if url.len > 0: 118 | var parsed = parseUri(url) 119 | if parsed.scheme.len == 0 or parsed.hostname.len == 0: 120 | parsed = parseUri("http://" & url) 121 | let auth = 122 | if parsed.username.len > 0: parsed.username & ":" & parsed.password 123 | else: "" 124 | return newProxy($parsed, auth) 125 | else: 126 | return nil 127 | 128 | proc getGccArch*(params: CliParams): int = 129 | ## Get gcc arch by getting pointer size x 8 130 | var 131 | outp = "" 132 | errC = 0 133 | 134 | when defined(windows): 135 | # Add MingW bin dir to PATH so getGccArch can find gcc. 136 | let pathEnv = getEnv("PATH") 137 | if not isDefaultCCInPath(params) and dirExists(params.getMingwBin()): 138 | putEnv("PATH", params.getMingwBin() & PathSep & pathEnv) 139 | 140 | (outp, errC) = execCmdEx("cmd /c echo int main^(^) { return sizeof^(void *^); } | gcc -xc - -o archtest && archtest") 141 | 142 | putEnv("PATH", pathEnv) 143 | else: 144 | (outp, errC) = execCmdEx("echo \"int main() { return sizeof(void *); }\" | gcc -xc - -o archtest && ./archtest") 145 | 146 | removeFile("archtest".addFileExt(ExeExt)) 147 | 148 | if errC in [4, 8]: 149 | return errC * 8 150 | else: 151 | # Fallback when arch detection fails. See https://github.com/dom96/choosenim/issues/284 152 | return when defined(windows): 32 else: 64 153 | 154 | proc getLatestCommit*(repo, branch: string): string = 155 | ## Get latest commit for remote Git repo with ls-remote 156 | ## 157 | ## Returns "" if Git isn't available 158 | let 159 | git = findExe("git") 160 | if git.len != 0: 161 | var 162 | cmd = when defined(windows): "cmd /c " else: "" 163 | cmd &= git.quoteShell & " ls-remote " & repo & " " & branch 164 | 165 | let 166 | (outp, errC) = execCmdEx(cmd) 167 | if errC == 0: 168 | for line in outp.splitLines(): 169 | result = line.split('\t')[0] 170 | break 171 | else: 172 | display("Warning", outp & "\ngit ls-remote failed", Warning, HighPriority) 173 | 174 | proc getNightliesUrl*(parsedContents: JsonNode, arch: int): (string, string) = 175 | let os = 176 | when defined(windows): "windows" 177 | elif defined(linux): "linux" 178 | elif defined(macosx): "osx" 179 | elif defined(freebsd): "freebsd" 180 | for jn in parsedContents.getElems(): 181 | if jn["name"].getStr().contains("devel"): 182 | let tagName = jn{"tag_name"}.getStr("") 183 | for asset in jn["assets"].getElems(): 184 | let aname = asset["name"].getStr() 185 | let url = asset{"browser_download_url"}.getStr("") 186 | if os in aname: 187 | when not defined(macosx): 188 | if "x" & $arch in aname: 189 | result = (url, tagName) 190 | else: 191 | result = (url, tagName) 192 | if result[0].len != 0: 193 | break 194 | if result[0].len != 0: 195 | break 196 | -------------------------------------------------------------------------------- /src/choosenimpkg/versions.nim: -------------------------------------------------------------------------------- 1 | import os, algorithm, sequtils 2 | 3 | import nimblepkg/version 4 | from nimblepkg/packageinfo import getNameVersion 5 | 6 | import download, cliparams, channel, switcher 7 | 8 | proc getLocalVersions(params: CliParams): seq[Version] = 9 | proc cmpVersions(x: Version, y: Version): int = 10 | if x == y: return 0 11 | if x < y: return -1 12 | return 1 13 | 14 | var localVersions: seq[Version] = @[] 15 | # check for the locally installed versions of Nim, 16 | for path in walkDirs(params.getInstallDir() & "/*"): 17 | let (_, version) = getNameVersion(path) 18 | let displayVersion = version.newVersion 19 | if isVersionInstalled(params, displayVersion): 20 | localVersions.add(displayVersion) 21 | localVersions.sort(cmpVersions, SortOrder.Descending) 22 | return localVersions 23 | 24 | proc getSpecialVersions*(params: CliParams): seq[Version] = 25 | var specialVersions = getLocalVersions(params) 26 | specialVersions.keepItIf(it.isSpecial()) 27 | return specialVersions 28 | 29 | proc getInstalledVersions*(params: CliParams): seq[Version] = 30 | var installedVersions = getLocalVersions(params) 31 | installedVersions.keepItIf(not it.isSpecial()) 32 | return installedVersions 33 | 34 | proc getAvailableVersions*(params: CliParams): seq[Version] = 35 | var releases = getOfficialReleases(params) 36 | return releases 37 | 38 | proc getCurrentVersion*(params: CliParams): Version = 39 | let path = getSelectedPath(params) 40 | let (_, currentVersion) = getNameVersion(path) 41 | return currentVersion.newVersion 42 | 43 | proc getLatestVersion*(params: CliParams): Version = 44 | let channel = getCurrentChannel(params) 45 | let latest = getChannelVersion(channel, params) 46 | return latest.newVersion 47 | 48 | proc isLatestVersion*(params: CliParams, version: Version): bool = 49 | let isLatest = (getLatestVersion(params) == version) 50 | return isLatest 51 | -------------------------------------------------------------------------------- /tests/tester.nim: -------------------------------------------------------------------------------- 1 | # Copyright (C) Dominik Picheta. All rights reserved. 2 | # BSD-3-Clause License. Look at license.txt for more info. 3 | import osproc, streams, unittest, strutils, os, sequtils, sugar, logging 4 | 5 | var rootDir = getCurrentDir() 6 | var exePath = rootDir / "bin" / addFileExt("choosenim", ExeExt) 7 | var nimbleDir = rootDir / "tests" / "nimbleDir" 8 | var choosenimDir = rootDir / "tests" / "choosenimDir" 9 | 10 | template cd*(dir: string, body: untyped) = 11 | ## Sets the current dir to ``dir``, executes ``body`` and restores the 12 | ## previous working dir. 13 | let lastDir = getCurrentDir() 14 | setCurrentDir(dir) 15 | body 16 | setCurrentDir(lastDir) 17 | 18 | template beginTest() = 19 | # Clear custom dirs. 20 | removeDir(nimbleDir) 21 | createDir(nimbleDir) 22 | removeDir(choosenimDir) 23 | createDir(choosenimDir) 24 | 25 | proc outputReader(stream: Stream, missedEscape: var bool): string = 26 | result = "" 27 | 28 | template handleEscape: untyped {.dirty.} = 29 | missedEscape = false 30 | result.add('\27') 31 | let escape = stream.readStr(1) 32 | result.add(escape) 33 | if escape[0] == '[': 34 | result.add(stream.readStr(2)) 35 | 36 | return 37 | 38 | # TODO: This would be much easier to implement if `peek` was supported. 39 | if missedEscape: 40 | handleEscape() 41 | 42 | while true: 43 | let c = stream.readStr(1) 44 | 45 | if c.len() == 0: 46 | return 47 | 48 | case c[0] 49 | of '\c', '\l': 50 | result.add(c[0]) 51 | return 52 | of '\27': 53 | if result.len > 0: 54 | missedEscape = true 55 | return 56 | 57 | handleEscape() 58 | else: 59 | result.add(c[0]) 60 | 61 | proc exec(args: varargs[string], exe=exePath, 62 | yes=true, liveOutput=false, 63 | global=false): tuple[output: string, exitCode: int] = 64 | var quotedArgs: seq[string] = @[exe] 65 | if yes: 66 | quotedArgs.add("-y") 67 | quotedArgs.add(@args) 68 | if not global: 69 | quotedArgs.add("--nimbleDir:" & nimbleDir) 70 | if exe.splitFile().name != "nimble": 71 | quotedArgs.add("--chooseNimDir:" & choosenimDir) 72 | quotedArgs.add("--noColor") 73 | 74 | for i in 0..quotedArgs.len-1: 75 | if " " in quotedArgs[i]: 76 | quotedArgs[i] = "\"" & quotedArgs[i] & "\"" 77 | 78 | echo "exec(): ", quotedArgs.join(" ") 79 | if not liveOutput: 80 | result = execCmdEx(quotedArgs.join(" ")) 81 | else: 82 | result.output = "" 83 | let process = startProcess(quotedArgs.join(" "), 84 | options={poEvalCommand, poStdErrToStdOut}) 85 | var missedEscape = false 86 | while true: 87 | if not process.outputStream.atEnd: 88 | let line = process.outputStream.outputReader(missedEscape) 89 | result.output.add(line) 90 | stdout.write(line) 91 | if line.len() != 0 and line[0] != '\27': 92 | stdout.flushFile() 93 | else: 94 | result.exitCode = process.peekExitCode() 95 | if result.exitCode != -1: break 96 | 97 | process.close() 98 | 99 | proc processOutput(output: string): seq[string] = 100 | output.strip.splitLines().filter((x: string) => (x.len > 0)) 101 | 102 | proc inLines(lines: seq[string], word: string): bool = 103 | for i in lines: 104 | if word.normalize in i.normalize: return true 105 | 106 | proc hasLine(lines: seq[string], line: string): bool = 107 | for i in lines: 108 | if i.normalize.strip() == line.normalize(): return true 109 | 110 | test "can compile choosenim": 111 | var args = @["build"] 112 | when defined(release): 113 | args.add "-d:release" 114 | when defined(staticBuild): 115 | args.add "-d:staticBuild" 116 | let (_, exitCode) = exec(args, exe="nimble", global=true, liveOutput=true) 117 | check exitCode == QuitSuccess 118 | 119 | test "refuses invalid path": 120 | beginTest() 121 | block: 122 | let (output, exitCode) = exec(getTempDir() / "blahblah") 123 | check exitCode == QuitFailure 124 | check inLines(output.processOutput, "invalid") 125 | check inLines(output.processOutput, "version") 126 | check inLines(output.processOutput, "path") 127 | 128 | block: 129 | let (output, exitCode) = exec(getTempDir()) 130 | check exitCode == QuitFailure 131 | check inLines(output.processOutput, "no") 132 | check inLines(output.processOutput, "binary") 133 | check inLines(output.processOutput, "found") 134 | 135 | test "fails on bad flag": 136 | beginTest() 137 | let (output, exitCode) = exec("--qwetqsdweqwe") 138 | check exitCode == QuitFailure 139 | check inLines(output.processOutput, "unknown") 140 | check inLines(output.processOutput, "flag") 141 | 142 | test "can choose #v1.0.0": 143 | beginTest() 144 | block: 145 | let (output, exitCode) = exec("\"#v1.0.0\"", liveOutput=true) 146 | check exitCode == QuitSuccess 147 | 148 | check inLines(output.processOutput, "building") 149 | check inLines(output.processOutput, "downloading") 150 | check inLines(output.processOutput, "building tools") 151 | check hasLine(output.processOutput, "switched to nim #v1.0.0") 152 | 153 | block: 154 | let (output, exitCode) = exec("\"#v1.0.0\"") 155 | check exitCode == QuitSuccess 156 | 157 | check hasLine(output.processOutput, "info: version #v1.0.0 already selected") 158 | 159 | # block: 160 | # let (output, exitCode) = exec("--version", exe=nimbleDir / "bin" / "nimble") 161 | # check exitCode == QuitSuccess 162 | # check inLines(output.processOutput, "v0.11.0") 163 | 164 | # Verify that we cannot remove currently selected #v1.0.0. 165 | block: 166 | let (output, exitCode) = exec(["remove", "\"#v1.0.0\""], liveOutput=true) 167 | check exitCode == QuitFailure 168 | 169 | check inLines(output.processOutput, "Cannot remove current version.") 170 | 171 | test "cannot remove not installed v0.16.0": 172 | beginTest() 173 | block: 174 | let (output, exitCode) = exec(["remove", "0.16.0"], liveOutput=true) 175 | check exitCode == QuitFailure 176 | 177 | check inLines(output.processOutput, "Version 0.16.0 is not installed.") 178 | 179 | when defined(linux): 180 | test "linux binary install": 181 | beginTest() 182 | block: 183 | let (output, exitCode) = exec("1.0.0", liveOutput=true) 184 | check exitCode == QuitSuccess 185 | 186 | check inLines(output.processOutput, "downloading") 187 | check inLines(output.processOutput, "already built") 188 | check hasLine(output.processOutput, "switched to nim 1.0.0") 189 | 190 | check not dirExists(choosenimDir / "toolchains" / "nim-1.0.0" / "c_code") 191 | 192 | test "can update devel with git": 193 | beginTest() 194 | block: 195 | let (output, exitCode) = exec(@["devel", "--latest"], liveOutput=true) 196 | 197 | check inLines(output.processOutput, "extracting") 198 | check inLines(output.processOutput, "setting") 199 | check inLines(output.processOutput, "latest changes") 200 | check inLines(output.processOutput, "building") 201 | 202 | if exitCode != QuitSuccess: 203 | # Let's be lenient here, latest Nim build could fail for any number of 204 | # reasons (HEAD could be broken). 205 | warn("Could not build latest `devel` of Nim, possibly a bug in choosenim") 206 | 207 | block: 208 | let (output, exitCode) = exec(@["update", "devel", "--latest"], liveOutput=true) 209 | 210 | # TODO: Below lines could fail in rare circumstances: if new commit is 211 | # made just after the above tests starts. 212 | # check not inLines(output.processOutput, "extracting") 213 | # check not inLines(output.processOutput, "setting") 214 | # TODO Disabling the above until https://github.com/nim-lang/Nim/pull/18945 215 | # is merged. 216 | check inLines(output.processOutput, "updating") 217 | check inLines(output.processOutput, "latest changes") 218 | check inLines(output.processOutput, "building") 219 | 220 | if exitCode != QuitSuccess: 221 | # Let's be lenient here, latest Nim build could fail for any number of 222 | # reasons (HEAD could be broken). 223 | warn("Could not build latest `devel` of Nim, possibly a bug in choosenim") 224 | 225 | test "can install and update nightlies": 226 | beginTest() 227 | block: 228 | # Install nightly 229 | let (output, exitCode) = exec("devel", liveOutput=true) 230 | 231 | # Travis runs into Github API limit 232 | if not inLines(output.processOutput, "unavailable"): 233 | check exitCode == QuitSuccess 234 | 235 | check inLines(output.processOutput, "devel from") 236 | check inLines(output.processOutput, "setting") 237 | when not defined(macosx): 238 | if not inLines(output.processOutput, "recent nightly"): 239 | check inLines(output.processOutput, "already built") 240 | check inLines(output.processOutput, "to Nim #devel") 241 | 242 | block: 243 | # Update nightly 244 | let (output, exitCode) = exec(@["update", "devel"], liveOutput=true) 245 | 246 | # Travis runs into Github API limit 247 | if not inLines(output.processOutput, "unavailable"): 248 | check exitCode == QuitSuccess 249 | 250 | check inLines(output.processOutput, "updating") 251 | check inLines(output.processOutput, "devel from") 252 | check inLines(output.processOutput, "setting") 253 | when not defined(macosx): 254 | if not inLines(output.processOutput, "recent nightly"): 255 | check inLines(output.processOutput, "already built") 256 | 257 | test "can update self": 258 | # updateSelf() doesn't use options --choosenimDir and --nimbleDir. It's used getAppDir(). 259 | # This will rewrite $project/bin dir, it's dangerous. 260 | # So, this test copy bin/choosenim to test/choosenimDir/choosenim, and use it. 261 | beginTest() 262 | let testExePath = choosenimDir / extractFilename(exePath) 263 | copyFileWithPermissions(exePath, testExePath) 264 | block : 265 | let (output, exitCode) = exec(["update", "self", "--debug", "--force"], exe=testExePath, liveOutput=true) 266 | check exitCode == QuitSuccess 267 | check inLines(output.processOutput, "Info: Updated choosenim to version") 268 | 269 | test "fails with invalid version": 270 | beginTest() 271 | block: 272 | let (output, exitCode) = exec("\"#version-1.6\"") 273 | check exitCode == QuitFailure 274 | check inLines(output.processOutput, "Version") 275 | check inLines(output.processOutput, "does not exist") 276 | 277 | test "can show general informations": 278 | beginTest() 279 | block: 280 | let (_, exitCode) = exec(@["stable"]) 281 | check exitCode == QuitSuccess 282 | block: 283 | let (output, exitCode) = exec(@["show"]) 284 | check exitCode == QuitSuccess 285 | check inLines(output.processOutput, "Selected:") 286 | check inLines(output.processOutput, "Channel: stable") 287 | check inLines(output.processOutput, "Path: " & choosenimDir) 288 | 289 | test "can show path": 290 | beginTest() 291 | block: 292 | let (_, exitCode) = exec(@["stable"]) 293 | check exitCode == QuitSuccess 294 | block: 295 | let (output, exitCode) = exec(@["show", "path"]) 296 | check exitCode == QuitSuccess 297 | echo output.processOutput 298 | check inLines(output.processOutput, choosenimDir) 299 | --------------------------------------------------------------------------------