diff --git a/.ci/create-static-binary.sh b/.ci/create-static-binary.sh new file mode 100755 index 000000000..27e934b6b --- /dev/null +++ b/.ci/create-static-binary.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +echo "Building binary in docker" + +docker build . -t schlez/nsw-static-binary + +echo "Copying to ./nsw-linux" + +docker run --rm -v $(pwd):$(pwd) --workdir $(pwd) schlez/nsw-static-binary cp /app/_build/default/executable/NswApp.exe ./nsw-linux diff --git a/.ci/esy-build-steps.yml b/.ci/esy-build-steps.yml index 7aad2d6b1..6f98189e9 100644 --- a/.ci/esy-build-steps.yml +++ b/.ci/esy-build-steps.yml @@ -4,8 +4,8 @@ steps: - task: NodeTool@0 inputs: versionSpec: '8.9' - - script: npm install -g esy@0.4.3 - displayName: 'npm install -g esy@0.4.3' + - script: npm install -g esy@latest + displayName: 'npm install -g esy@latest' - script: esy install displayName: 'esy install' - script: esy pesy @@ -14,7 +14,7 @@ steps: displayName: 'esy build' - script: esy test displayName: 'esy test' - - script: esy x NswApp.exe + - script: esy x nsw.exe displayName: 'Run the main binary' - script: esy ls-libs continueOnError: true diff --git a/.ci/prepare-static-build.sh b/.ci/prepare-static-build.sh new file mode 100644 index 000000000..f84570ebd --- /dev/null +++ b/.ci/prepare-static-build.sh @@ -0,0 +1 @@ +sed -i 's@"flags": \[\]@"flags": ["-ccopt", "-static"]@' package.json diff --git a/.ci/shasum b/.ci/shasum new file mode 100755 index 000000000..1d9c32d31 --- /dev/null +++ b/.ci/shasum @@ -0,0 +1,3 @@ +#!/bin/bash +shift 2 +sha1sum $@ diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..7ac74dd4e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +_build +_esy +node_modules +Dockerfile diff --git a/.gitignore b/.gitignore index b07980916..d4d1fb963 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ _esy/ nsw.install .DS_Store *.install +.tmp diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..9abc7c555 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM frolvlad/alpine-glibc + +RUN apk add --no-cache nodejs bash npm curl g++ make m4 patch gmp-dev perl git jq +ADD .ci/shasum /usr/bin/shasum + +USER root + +RUN npm -g config set user root +RUN npm i -g esy@latest + +WORKDIR /app +ADD . /app + +RUN jq '. | .buildDirs.executable.flags |= . + ["-ccopt", "-static"]' package.json > package.json.new && mv package.json.new package.json +RUN npx esy i +RUN npx esy pesy +RUN npx esy b +RUN npx esy test \ No newline at end of file diff --git a/README.md b/README.md index 7594aa5f6..835ebbceb 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,45 @@ -# nsw +

+ Node Switcher (nsw) Build Status +

+> A simple and fast `nvm` replacement, built in native ReasonML. -[![CircleCI](https://circleci.com/gh/yourgithubhandle/nsw/tree/master.svg?style=svg)](https://circleci.com/gh/yourgithubhandle/nsw/tree/master) +:rocket: Single executable :rocket: Blazing fast :rocket: +![Blazing fast](./docs/nvm_comparison.png) -**Contains the following libraries and executables:** +## Features +- Single file, easy installation :sparkles: +- Fast fast fast fast :rocket: +- Install multiple node versions without a hassle! :clap: +- [Project-specific `.nvmrc` file support](./features_tests/nvmrc) -``` -nsw@0.0.0 -│ -├─test/ -│ name: TestNsw.exe -│ main: TestNsw -│ require: nsw.lib -│ -├─library/ -│ library name: nsw.lib -│ namespace: Nsw -│ require: -│ -└─executable/ - name: NswApp.exe - main: NswApp - require: nsw.lib -``` +## Installation + +* Download the [latest release binary](https://github.com/Schniz/nsw/releases) for your system +* Make it available globally on `$PATH` +* Add the following line to your `.bashrc`/`.zshrc` file: + + ```bash + eval `nsw env` + ``` + +## TODO +- [ ] Feature: make versions complete the latest: `10` would infer the latest minor and patch versions of node 10. `10.1` would infer the latest patch version of node 10.1 +- [ ] Feature: `nsw use --install`, `nsw use --quiet` +- [ ] Feature: `nsw install lts`? +- [ ] Feature: `nsw alias`? +- [ ] Feature: Consider nvm-like per-shell usage with symlinks on `/tmp` directory +- [ ] OSX: Add to homebrew? +- [ ] Windows Support? +- [ ] Linux: Replace `curl` usage with `cohttp`/`ocurl` or something else which is statically-linkable +- [ ] Linux: Replace `tar` with a statically linked library too (for ungzip + untar) ## Developing: ``` npm install -g esy -git clone +git clone https://github.com/Schniz/nsw.git esy install esy build ``` @@ -39,12 +49,15 @@ esy build After building the project, you can run the main binary that is produced. ``` -esy x NswApp.exe +esy x nsw.exe ``` ## Running Tests: ``` -# Runs the "test" command in `package.json`. +# Runs some smoke-unity test esy test + +# Runs the feature tests +feature_tests/run.sh ``` diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 820a19942..5b0aee121 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -17,8 +17,16 @@ jobs: steps: # TODO: Uncomment both this and 'publish-build-cache' below to enable build caching for Linux. # - template: .ci/restore-build-cache.yml - - template: .ci/esy-build-steps.yml + - script: .ci/create-static-binary.sh + - script: ./feature_tests/run.sh $(pwd)/nsw-linux + # - script: bash .ci/prepare-static-build.sh + # - template: .ci/esy-build-steps.yml # - template: .ci/publish-build-cache.yml + - task: PublishBuildArtifacts@1 + displayName: 'Save artifact' + inputs: + PathtoPublish: 'nsw-linux' + ArtifactName: nsw-linux - job: MacOS timeoutInMinutes: 0 @@ -33,21 +41,28 @@ jobs: # TODO: Uncomment both this and 'publish-build-cache' below to enable build caching for Mac. # - template: .ci/restore-build-cache.yml - template: .ci/esy-build-steps.yml + - script: cp _build/default/executable/NswApp.exe _build/nsw + - script: ./feature_tests/run.sh $(pwd)/_build/nsw # - template: .ci/publish-build-cache.yml + - task: PublishBuildArtifacts@1 + displayName: 'Save artifact' + inputs: + PathtoPublish: '_build/nsw' + ArtifactName: nsw-macos -- job: Windows - timeoutInMinutes: 0 - pool: - vmImage: 'vs2017-win2016' +# - job: Windows +# timeoutInMinutes: 0 +# pool: +# vmImage: 'vs2017-win2016' - variables: - ESY__CACHE_INSTALL_PATH: C:\Users\VssAdministrator\.esy\3_\i - ESY__CACHE_SOURCE_TARBALL_PATH: C:\Users\VssAdministrator\.esy\source\i +# variables: +# ESY__CACHE_INSTALL_PATH: C:\Users\VssAdministrator\.esy\3_\i +# ESY__CACHE_SOURCE_TARBALL_PATH: C:\Users\VssAdministrator\.esy\source\i - steps: - - template: .ci/restore-build-cache.yml - - template: .ci/esy-build-steps.yml - - template: .ci/publish-build-cache.yml +# steps: +# - template: .ci/restore-build-cache.yml +# - template: .ci/esy-build-steps.yml +# - template: .ci/publish-build-cache.yml - job: Release timeoutInMinutes: 0 @@ -55,7 +70,7 @@ jobs: dependsOn: - Linux - MacOS - - Windows + # - Windows condition: succeeded() pool: vmImage: ubuntu-16.04 diff --git a/docs/nvm_comparison.png b/docs/nvm_comparison.png new file mode 100644 index 000000000..7c3c2913f Binary files /dev/null and b/docs/nvm_comparison.png differ diff --git a/esy.lock/index.json b/esy.lock/index.json index 6251a8fd7..fa96723f4 100644 --- a/esy.lock/index.json +++ b/esy.lock/index.json @@ -1,5 +1,5 @@ { - "checksum": "fc3353f4500533a4dfc452ed120be173", + "checksum": "bc7707f147bdaf3b3f87430bb4b3f0cd", "root": "nsw@link:./package.json", "node": { "refmterr@3.1.10@d41d8cd9": { @@ -15,7 +15,7 @@ "overrides": [], "dependencies": [ "ocaml@4.6.9@d41d8cd9", "@opam/re@opam:1.7.3@83095efd", - "@opam/dune@opam:1.6.3@a7d7baed", "@esy-ocaml/reason@3.3.7@d41d8cd9" + "@opam/dune@opam:1.6.3@a7d7baed", "@esy-ocaml/reason@3.4.0@d41d8cd9" ], "devDependencies": [] }, @@ -55,46 +55,87 @@ "overrides": [], "dependencies": [ "refmterr@3.1.10@d41d8cd9", "pesy@0.4.1@d41d8cd9", - "ocaml@4.6.9@d41d8cd9", "@reason-native/pastel@0.0.0@d41d8cd9", - "@reason-native/console@0.0.1@d41d8cd9", + "ocaml@4.6.9@d41d8cd9", "@reason-native/rely@1.0.1@d41d8cd9", + "@reason-native/pastel@0.0.1@d41d8cd9", + "@reason-native/console@0.0.2@d41d8cd9", + "@opam/semver@opam:0.1.0@595ed2e0", "@opam/ppx_let@opam:v0.11.0@15f51b1c", "@opam/lwt_ppx@opam:1.2.1@db1172a7", "@opam/lwt@opam:4.1.0@111fc2bf", + "@opam/lambdasoup@opam:0.6.3@b8ef0a81", "@opam/dune@opam:1.6.3@a7d7baed", "@opam/core@opam:v0.11.3@ac79d7b5", - "@esy-ocaml/reason@3.3.7@d41d8cd9" + "@opam/cmdliner@opam:1.0.3@96d31520", + "@esy-ocaml/reason@3.4.0@d41d8cd9" ], "devDependencies": [ "@opam/merlin@opam:3.2.2@829ee6dd" ] }, - "@reason-native/pastel@0.0.0@d41d8cd9": { - "id": "@reason-native/pastel@0.0.0@d41d8cd9", - "name": "@reason-native/pastel", - "version": "0.0.0", + "@reason-native/rely@1.0.1@d41d8cd9": { + "id": "@reason-native/rely@1.0.1@d41d8cd9", + "name": "@reason-native/rely", + "version": "1.0.1", "source": { "type": "install", "source": [ - "archive:https://registry.npmjs.org/@reason-native/pastel/-/pastel-0.0.0.tgz#sha1:44f357fea33b894c9fbf651ecb3f2029e04ee013" + "archive:https://registry.npmjs.org/@reason-native/rely/-/rely-1.0.1.tgz#sha1:14afdbf5bada7739dd9a68d4817c53e7d5ddd50c" ] }, "overrides": [], "dependencies": [ "refmterr@3.1.10@d41d8cd9", "ocaml@4.6.9@d41d8cd9", - "@opam/dune@opam:1.6.3@a7d7baed", "@esy-ocaml/reason@3.3.7@d41d8cd9" + "@reason-native/pastel@0.0.1@d41d8cd9", + "@reason-native/file-context-printer@0.0.2@d41d8cd9", + "@opam/dune@opam:1.6.3@a7d7baed", "@esy-ocaml/reason@3.4.0@d41d8cd9" ], "devDependencies": [] }, - "@reason-native/console@0.0.1@d41d8cd9": { - "id": "@reason-native/console@0.0.1@d41d8cd9", - "name": "@reason-native/console", + "@reason-native/pastel@0.0.1@d41d8cd9": { + "id": "@reason-native/pastel@0.0.1@d41d8cd9", + "name": "@reason-native/pastel", "version": "0.0.1", "source": { "type": "install", "source": [ - "archive:https://registry.npmjs.org/@reason-native/console/-/console-0.0.1.tgz#sha1:08a6786dd34a1aa326f88a5cf3c9819df3339fb1" + "archive:https://registry.npmjs.org/@reason-native/pastel/-/pastel-0.0.1.tgz#sha1:ff305233ffd915d317cdcebee534d16c0aada198" ] }, "overrides": [], "dependencies": [ - "refmterr@3.1.10@d41d8cd9", "ocaml@4.6.9@d41d8cd9", - "@opam/dune@opam:1.6.3@a7d7baed", "@esy-ocaml/reason@3.3.7@d41d8cd9" + "ocaml@4.6.9@d41d8cd9", "@opam/dune@opam:1.6.3@a7d7baed", + "@esy-ocaml/reason@3.4.0@d41d8cd9" + ], + "devDependencies": [] + }, + "@reason-native/file-context-printer@0.0.2@d41d8cd9": { + "id": "@reason-native/file-context-printer@0.0.2@d41d8cd9", + "name": "@reason-native/file-context-printer", + "version": "0.0.2", + "source": { + "type": "install", + "source": [ + "archive:https://registry.npmjs.org/@reason-native/file-context-printer/-/file-context-printer-0.0.2.tgz#sha1:effbcb6db360cca9ac293a0075bc0d5912bd0ce0" + ] + }, + "overrides": [], + "dependencies": [ + "ocaml@4.6.9@d41d8cd9", "@reason-native/pastel@0.0.1@d41d8cd9", + "@opam/re@opam:1.7.3@83095efd", "@opam/dune@opam:1.6.3@a7d7baed", + "@esy-ocaml/reason@3.4.0@d41d8cd9" + ], + "devDependencies": [] + }, + "@reason-native/console@0.0.2@d41d8cd9": { + "id": "@reason-native/console@0.0.2@d41d8cd9", + "name": "@reason-native/console", + "version": "0.0.2", + "source": { + "type": "install", + "source": [ + "archive:https://registry.npmjs.org/@reason-native/console/-/console-0.0.2.tgz#sha1:25bd391653579a56d53ddf7cc502a237b784163b" + ] + }, + "overrides": [], + "dependencies": [ + "ocaml@4.6.9@d41d8cd9", "@opam/dune@opam:1.6.3@a7d7baed", + "@esy-ocaml/reason@3.4.0@d41d8cd9" ], "devDependencies": [] }, @@ -156,6 +197,58 @@ "@opam/base@opam:v0.11.1@0e54024e" ] }, + "@opam/uutf@opam:1.0.1@c4650647": { + "id": "@opam/uutf@opam:1.0.1@c4650647", + "name": "@opam/uutf", + "version": "opam:1.0.1", + "source": { + "type": "install", + "source": [ + "archive:https://opam.ocaml.org/cache/md5/b8/b8535f974027357094c5cdb4bf03a21b#md5:b8535f974027357094c5cdb4bf03a21b", + "archive:http://erratique.ch/software/uutf/releases/uutf-1.0.1.tbz#md5:b8535f974027357094c5cdb4bf03a21b" + ], + "opam": { + "name": "uutf", + "version": "1.0.1", + "path": "esy.lock/opam/uutf.1.0.1" + } + }, + "overrides": [], + "dependencies": [ + "ocaml@4.6.9@d41d8cd9", "@opam/uchar@opam:0.0.2@c8218eea", + "@opam/topkg@opam:1.0.0@61f4ccf9", + "@opam/ocamlfind@opam:1.8.0@96572762", + "@opam/ocamlbuild@opam:0.12.0@6c616094", + "@opam/cmdliner@opam:1.0.3@96d31520", + "@esy-ocaml/substs@0.0.1@d41d8cd9" + ], + "devDependencies": [ + "ocaml@4.6.9@d41d8cd9", "@opam/uchar@opam:0.0.2@c8218eea" + ] + }, + "@opam/uchar@opam:0.0.2@c8218eea": { + "id": "@opam/uchar@opam:0.0.2@c8218eea", + "name": "@opam/uchar", + "version": "opam:0.0.2", + "source": { + "type": "install", + "source": [ + "archive:https://opam.ocaml.org/cache/md5/c9/c9ba2c738d264c420c642f7bb1cf4a36#md5:c9ba2c738d264c420c642f7bb1cf4a36", + "archive:https://github.com/ocaml/uchar/releases/download/v0.0.2/uchar-0.0.2.tbz#md5:c9ba2c738d264c420c642f7bb1cf4a36" + ], + "opam": { + "name": "uchar", + "version": "0.0.2", + "path": "esy.lock/opam/uchar.0.0.2" + } + }, + "overrides": [], + "dependencies": [ + "ocaml@4.6.9@d41d8cd9", "@opam/ocamlbuild@opam:0.12.0@6c616094", + "@esy-ocaml/substs@0.0.1@d41d8cd9" + ], + "devDependencies": [ "ocaml@4.6.9@d41d8cd9" ] + }, "@opam/typerep@opam:v0.11.0@625676b6": { "id": "@opam/typerep@opam:v0.11.0@625676b6", "name": "@opam/typerep", @@ -182,6 +275,34 @@ "ocaml@4.6.9@d41d8cd9", "@opam/base@opam:v0.11.1@0e54024e" ] }, + "@opam/topkg@opam:1.0.0@61f4ccf9": { + "id": "@opam/topkg@opam:1.0.0@61f4ccf9", + "name": "@opam/topkg", + "version": "opam:1.0.0", + "source": { + "type": "install", + "source": [ + "archive:https://opam.ocaml.org/cache/md5/e3/e3d76bda06bf68cb5853caf6627da603#md5:e3d76bda06bf68cb5853caf6627da603", + "archive:http://erratique.ch/software/topkg/releases/topkg-1.0.0.tbz#md5:e3d76bda06bf68cb5853caf6627da603" + ], + "opam": { + "name": "topkg", + "version": "1.0.0", + "path": "esy.lock/opam/topkg.1.0.0" + } + }, + "overrides": [], + "dependencies": [ + "ocaml@4.6.9@d41d8cd9", "@opam/result@opam:1.3@bee8bf2e", + "@opam/ocamlfind@opam:1.8.0@96572762", + "@opam/ocamlbuild@opam:0.12.0@6c616094", + "@esy-ocaml/substs@0.0.1@d41d8cd9" + ], + "devDependencies": [ + "ocaml@4.6.9@d41d8cd9", "@opam/result@opam:1.3@bee8bf2e", + "@opam/ocamlbuild@opam:0.12.0@6c616094" + ] + }, "@opam/stdio@opam:v0.11.0@3b11cb88": { "id": "@opam/stdio@opam:v0.11.0@3b11cb88", "name": "@opam/stdio", @@ -312,6 +433,30 @@ "@opam/parsexp@opam:v0.11.0@7febd99d", "@opam/num@opam:1.1@dcdca088" ] }, + "@opam/semver@opam:0.1.0@595ed2e0": { + "id": "@opam/semver@opam:0.1.0@595ed2e0", + "name": "@opam/semver", + "version": "opam:0.1.0", + "source": { + "type": "install", + "source": [ + "archive:https://opam.ocaml.org/cache/md5/ce/ce6614ba2f91754028b29a12989f9da6#md5:ce6614ba2f91754028b29a12989f9da6", + "archive:https://github.com/rgrinberg/ocaml-semver/archive/v0.1.0.tar.gz#md5:ce6614ba2f91754028b29a12989f9da6" + ], + "opam": { + "name": "semver", + "version": "0.1.0", + "path": "esy.lock/opam/semver.0.1.0" + } + }, + "overrides": [], + "dependencies": [ + "ocaml@4.6.9@d41d8cd9", "@opam/ocamlfind@opam:1.8.0@96572762", + "@opam/ocamlbuild@opam:0.12.0@6c616094", + "@esy-ocaml/substs@0.0.1@d41d8cd9" + ], + "devDependencies": [ "ocaml@4.6.9@d41d8cd9" ] + }, "@opam/result@opam:1.3@bee8bf2e": { "id": "@opam/result@opam:1.3@bee8bf2e", "name": "@opam/result", @@ -1517,6 +1662,33 @@ ], "devDependencies": [ "ocaml@4.6.9@d41d8cd9" ] }, + "@opam/markup@opam:0.8.0@e4958f14": { + "id": "@opam/markup@opam:0.8.0@e4958f14", + "name": "@opam/markup", + "version": "opam:0.8.0", + "source": { + "type": "install", + "source": [ + "archive:https://opam.ocaml.org/cache/md5/be/be0e44a8e8a540f633996e0e26109b4d#md5:be0e44a8e8a540f633996e0e26109b4d", + "archive:https://github.com/aantron/markup.ml/archive/0.8.0.tar.gz#md5:be0e44a8e8a540f633996e0e26109b4d" + ], + "opam": { + "name": "markup", + "version": "0.8.0", + "path": "esy.lock/opam/markup.0.8.0" + } + }, + "overrides": [], + "dependencies": [ + "ocaml@4.6.9@d41d8cd9", "@opam/uutf@opam:1.0.1@c4650647", + "@opam/uchar@opam:0.0.2@c8218eea", "@opam/dune@opam:1.6.3@a7d7baed", + "@esy-ocaml/substs@0.0.1@d41d8cd9" + ], + "devDependencies": [ + "ocaml@4.6.9@d41d8cd9", "@opam/uutf@opam:1.0.1@c4650647", + "@opam/uchar@opam:0.0.2@c8218eea" + ] + }, "@opam/lwt_ppx@opam:1.2.1@db1172a7": { "id": "@opam/lwt_ppx@opam:1.2.1@db1172a7", "name": "@opam/lwt_ppx", @@ -1579,6 +1751,32 @@ "ocaml@4.6.9@d41d8cd9", "@opam/result@opam:1.3@bee8bf2e" ] }, + "@opam/lambdasoup@opam:0.6.3@b8ef0a81": { + "id": "@opam/lambdasoup@opam:0.6.3@b8ef0a81", + "name": "@opam/lambdasoup", + "version": "opam:0.6.3", + "source": { + "type": "install", + "source": [ + "archive:https://opam.ocaml.org/cache/md5/89/89f0596aa05a6e7a33bf9d74797905f1#md5:89f0596aa05a6e7a33bf9d74797905f1", + "archive:https://github.com/aantron/lambda-soup/archive/0.6.3.tar.gz#md5:89f0596aa05a6e7a33bf9d74797905f1" + ], + "opam": { + "name": "lambdasoup", + "version": "0.6.3", + "path": "esy.lock/opam/lambdasoup.0.6.3" + } + }, + "overrides": [], + "dependencies": [ + "ocaml@4.6.9@d41d8cd9", "@opam/markup@opam:0.8.0@e4958f14", + "@opam/jbuilder@opam:transition@58bdfe0a", + "@esy-ocaml/substs@0.0.1@d41d8cd9" + ], + "devDependencies": [ + "ocaml@4.6.9@d41d8cd9", "@opam/markup@opam:0.8.0@e4958f14" + ] + }, "@opam/jbuilder@opam:transition@58bdfe0a": { "id": "@opam/jbuilder@opam:transition@58bdfe0a", "name": "@opam/jbuilder", @@ -1903,6 +2101,28 @@ "dependencies": [ "@esy-ocaml/substs@0.0.1@d41d8cd9" ], "devDependencies": [] }, + "@opam/cmdliner@opam:1.0.3@96d31520": { + "id": "@opam/cmdliner@opam:1.0.3@96d31520", + "name": "@opam/cmdliner", + "version": "opam:1.0.3", + "source": { + "type": "install", + "source": [ + "archive:https://opam.ocaml.org/cache/md5/36/3674ad01d4445424105d33818c78fba8#md5:3674ad01d4445424105d33818c78fba8", + "archive:http://erratique.ch/software/cmdliner/releases/cmdliner-1.0.3.tbz#md5:3674ad01d4445424105d33818c78fba8" + ], + "opam": { + "name": "cmdliner", + "version": "1.0.3", + "path": "esy.lock/opam/cmdliner.1.0.3" + } + }, + "overrides": [], + "dependencies": [ + "ocaml@4.6.9@d41d8cd9", "@esy-ocaml/substs@0.0.1@d41d8cd9" + ], + "devDependencies": [ "ocaml@4.6.9@d41d8cd9" ] + }, "@opam/biniou@opam:1.2.0@c8516f18": { "id": "@opam/biniou@opam:1.2.0@c8516f18", "name": "@opam/biniou", @@ -2049,14 +2269,14 @@ "dependencies": [], "devDependencies": [] }, - "@esy-ocaml/reason@3.3.7@d41d8cd9": { - "id": "@esy-ocaml/reason@3.3.7@d41d8cd9", + "@esy-ocaml/reason@3.4.0@d41d8cd9": { + "id": "@esy-ocaml/reason@3.4.0@d41d8cd9", "name": "@esy-ocaml/reason", - "version": "3.3.7", + "version": "3.4.0", "source": { "type": "install", "source": [ - "archive:https://registry.npmjs.org/@esy-ocaml/reason/-/reason-3.3.7.tgz#sha1:4d75b8876807c4178c6fff2359962066bb69d944" + "archive:https://registry.npmjs.org/@esy-ocaml/reason/-/reason-3.4.0.tgz#sha1:8c84c183a95d489a3e82ff0465effe4b56ff12af" ] }, "overrides": [], diff --git a/esy.lock/opam/cmdliner.1.0.3/opam b/esy.lock/opam/cmdliner.1.0.3/opam new file mode 100644 index 000000000..661a34be7 --- /dev/null +++ b/esy.lock/opam/cmdliner.1.0.3/opam @@ -0,0 +1,36 @@ +opam-version: "2.0" +maintainer: "Daniel Bünzli " +authors: ["Daniel Bünzli "] +homepage: "http://erratique.ch/software/cmdliner" +doc: "http://erratique.ch/software/cmdliner/doc/Cmdliner" +dev-repo: "git+http://erratique.ch/repos/cmdliner.git" +bug-reports: "https://github.com/dbuenzli/cmdliner/issues" +tags: [ "cli" "system" "declarative" "org:erratique" ] +license: "ISC" +depends:[ "ocaml" {>= "4.03.0"} ] +build: [[ make "all" "PREFIX=%{prefix}%" ]] +install: +[[make "install" "LIBDIR=%{_:lib}%" "DOCDIR=%{_:doc}%" ] + [make "install-doc" "LIBDIR=%{_:lib}%" "DOCDIR=%{_:doc}%" ]] + +synopsis: """Declarative definition of command line interfaces for OCaml""" +description: """\ + +Cmdliner allows the declarative definition of command line interfaces +for OCaml. + +It provides a simple and compositional mechanism to convert command +line arguments to OCaml values and pass them to your functions. The +module automatically handles syntax errors, help messages and UNIX man +page generation. It supports programs with single or multiple commands +and respects most of the [POSIX][1] and [GNU][2] conventions. + +Cmdliner has no dependencies and is distributed under the ISC license. + +[1]: http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap12.html +[2]: http://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html +""" +url { +archive: "http://erratique.ch/software/cmdliner/releases/cmdliner-1.0.3.tbz" +checksum: "3674ad01d4445424105d33818c78fba8" +} diff --git a/esy.lock/opam/lambdasoup.0.6.3/opam b/esy.lock/opam/lambdasoup.0.6.3/opam new file mode 100644 index 000000000..f27c0679e --- /dev/null +++ b/esy.lock/opam/lambdasoup.0.6.3/opam @@ -0,0 +1,35 @@ +opam-version: "2.0" +version: "0.6.3" +homepage: "https://github.com/aantron/lambda-soup" +doc: "http://aantron.github.io/lambda-soup" +bug-reports: "https://github.com/aantron/lambda-soup/issues" +license: "BSD" + +authors: "Anton Bachin " +maintainer: "Anton Bachin " +dev-repo: "git+https://github.com/aantron/lambda-soup.git" +depends: [ + "ocaml" + "jbuilder" {build & >= "1.0+beta10"} + "markup" {>= "0.7.1"} + "ounit" {with-test} +] +build: [ + ["jbuilder" "build" "-p" name "-j" jobs] +] +synopsis: "Easy functional HTML scraping and manipulation with CSS selectors" +description: """ +Lambda Soup is an HTML scraping library inspired by Python's Beautiful Soup. It +provides lazy traversals from HTML nodes to their parents, children, siblings, +etc., and to nodes matching CSS selectors. The traversals can be manipulated +using standard functional combinators such as fold, filter, and map. + +The DOM tree is mutable. You can use Lambda Soup for automatic HTML rewriting in +scripts. Lambda Soup rewrites its own ocamldoc page this way. + +A major goal of Lambda Soup is to be easy to use, including in interactive +sessions, and to have a minimal learning curve. It is a very simple library.""" +url { + src: "https://github.com/aantron/lambda-soup/archive/0.6.3.tar.gz" + checksum: "md5=89f0596aa05a6e7a33bf9d74797905f1" +} diff --git a/esy.lock/opam/markup.0.8.0/opam b/esy.lock/opam/markup.0.8.0/opam new file mode 100644 index 000000000..a22644c9d --- /dev/null +++ b/esy.lock/opam/markup.0.8.0/opam @@ -0,0 +1,52 @@ +opam-version: "2.0" +version: "0.8.0" + +maintainer: "Anton Bachin " +authors: "Anton Bachin " +homepage: "https://github.com/aantron/markup.ml" +doc: "http://aantron.github.io/markup.ml" +bug-reports: "https://github.com/aantron/markup.ml/issues" +dev-repo: "git+https://github.com/aantron/markup.ml.git" +license: "BSD" + +depends: [ + "ocaml" + "dune" {build} + "ounit" {with-test} + "uchar" + "uutf" {>= "1.0.0"} +] +# Markup.ml implicitly requires OCaml 4.02.3, as this is a contraint of Dune. +# Without that, Markup.ml implicitly requires OCaml 4.01.0, as this is a +# constraint of Uutf. Without *that*, Markup.ml works on OCaml 3.11 or searlier. + +build: [ + ["dune" "build" "-p" name "-j" jobs] +] + +synopsis: "Error-recovering functional HTML5 and XML parsers and writers" + +description: """ +Markup.ml provides an HTML parser and an XML parser. The parsers are wrapped in +a simple interface: they are functions that transform byte streams to parsing +signal streams. Streams can be manipulated in various ways, such as processing +by fold, filter, and map, assembly into DOM tree structures, or serialization +back to HTML or XML. + +Both parsers are based on their respective standards. The HTML parser, in +particular, is based on the state machines defined in HTML5. + +The parsers are error-recovering by default, and accept fragments. This makes it +very easy to get a best-effort parse of some input. The parsers can, however, be +easily configured to be strict, and to accept only full documents. + +Apart from this, the parsers are streaming (do not build up a document in +memory), non-blocking (can be used with threading libraries), lazy (do not +consume input unless the signal stream is being read), and process the input in +a single pass. They automatically detect the character encoding of the input +stream, and convert everything to UTF-8.""" + +url { + src: "https://github.com/aantron/markup.ml/archive/0.8.0.tar.gz" + checksum: "md5=be0e44a8e8a540f633996e0e26109b4d" +} diff --git a/esy.lock/opam/semver.0.1.0/opam b/esy.lock/opam/semver.0.1.0/opam new file mode 100644 index 000000000..3ee31e3ee --- /dev/null +++ b/esy.lock/opam/semver.0.1.0/opam @@ -0,0 +1,35 @@ +opam-version: "2.0" +maintainer: "rudi.grinberg@gmail.com" +authors: [ + "Tikhon Jelvis" + "Rudi Grinberg" +] +homepage: "https://github.com/rgrinberg/ocaml-semver" +bug-reports: "https://github.com/rgrinberg/ocaml-semver/issues" +license: "BSD3" +dev-repo: "git+https://github.com/rgrinberg/ocaml-semver.git" +build: [ + ["ocaml" "setup.ml" "-configure"] + ["ocaml" "setup.ml" "-build"] + ["ocaml" "setup.ml" "-configure" "--enable-tests"] {with-test} + ["ocaml" "setup.ml" "-build"] {with-test} + ["ocaml" "setup.ml" "-test"] {with-test} + ["ocaml" "setup.ml" "-doc"] {with-doc} +] +install: ["ocaml" "setup.ml" "-install"] +remove: ["ocamlfind" "remove" "semver"] +depends: [ + "ocaml" {>= "4.02.0"} + "ocamlfind" {build} + "ounit" {with-test} + "ocamlbuild" {build} +] +synopsis: "Semantic versioning module" +description: """ +Provides a single module `Semver` that can parse, compare, and manipulate +software versions of the form x.x.x. See http://semver.org/""" +flags: light-uninstall +url { + src: "https://github.com/rgrinberg/ocaml-semver/archive/v0.1.0.tar.gz" + checksum: "md5=ce6614ba2f91754028b29a12989f9da6" +} diff --git a/esy.lock/opam/topkg.1.0.0/opam b/esy.lock/opam/topkg.1.0.0/opam new file mode 100644 index 000000000..2276edb33 --- /dev/null +++ b/esy.lock/opam/topkg.1.0.0/opam @@ -0,0 +1,49 @@ +opam-version: "2.0" +maintainer: "Daniel Bünzli " +authors: ["Daniel Bünzli "] +homepage: "http://erratique.ch/software/topkg" +doc: "http://erratique.ch/software/topkg/doc" +license: "ISC" +dev-repo: "git+http://erratique.ch/repos/topkg.git" +bug-reports: "https://github.com/dbuenzli/topkg/issues" +tags: ["packaging" "ocamlbuild" "org:erratique"] +depends: [ + "ocaml" {>= "4.01.0"} + "ocamlfind" {build & >= "1.6.1"} + "ocamlbuild" + "result" ] +build: [[ + "ocaml" "pkg/pkg.ml" "build" + "--pkg-name" name + "--dev-pkg" "%{pinned}%" ]] +synopsis: """The transitory OCaml software packager""" +description: """\ + +Topkg is a packager for distributing OCaml software. It provides an +API to describe the files a package installs in a given build +configuration and to specify information about the package's +distribution, creation and publication procedures. + +The optional topkg-care package provides the `topkg` command line tool +which helps with various aspects of a package's life cycle: creating +and linting a distribution, releasing it on the WWW, publish its +documentation, add it to the OCaml opam repository, etc. + +Topkg is distributed under the ISC license and has **no** +dependencies. This is what your packages will need as a *build* +dependency. + +Topkg-care is distributed under the ISC license it depends on +[fmt][fmt], [logs][logs], [bos][bos], [cmdliner][cmdliner], +[webbrowser][webbrowser] and `opam-format`. + +[fmt]: http://erratique.ch/software/fmt +[logs]: http://erratique.ch/software/logs +[bos]: http://erratique.ch/software/bos +[cmdliner]: http://erratique.ch/software/cmdliner +[webbrowser]: http://erratique.ch/software/webbrowser +""" +url { +src: "http://erratique.ch/software/topkg/releases/topkg-1.0.0.tbz" +checksum: "md5=e3d76bda06bf68cb5853caf6627da603" +} diff --git a/esy.lock/opam/uchar.0.0.2/opam b/esy.lock/opam/uchar.0.0.2/opam new file mode 100644 index 000000000..428d7aa6f --- /dev/null +++ b/esy.lock/opam/uchar.0.0.2/opam @@ -0,0 +1,36 @@ +opam-version: "2.0" +maintainer: "Daniel Bünzli " +authors: ["Daniel Bünzli "] +homepage: "http://ocaml.org" +doc: "https://ocaml.github.io/uchar/" +dev-repo: "git+https://github.com/ocaml/uchar.git" +bug-reports: "https://github.com/ocaml/uchar/issues" +tags: [ "text" "character" "unicode" "compatibility" "org:ocaml.org" ] +license: "typeof OCaml system" +depends: [ + "ocaml" {>= "3.12.0"} + "ocamlbuild" {build} +] +build: [ + ["ocaml" "pkg/git.ml"] + [ + "ocaml" + "pkg/build.ml" + "native=%{ocaml:native}%" + "native-dynlink=%{ocaml:native-dynlink}%" + ] +] +synopsis: "Compatibility library for OCaml's Uchar module" +description: """ +The `uchar` package provides a compatibility library for the +[`Uchar`][1] module introduced in OCaml 4.03. + +The `uchar` package is distributed under the license of the OCaml +compiler. See [LICENSE](LICENSE) for details. + +[1]: http://caml.inria.fr/pub/docs/manual-ocaml/libref/Uchar.html""" +url { + src: + "https://github.com/ocaml/uchar/releases/download/v0.0.2/uchar-0.0.2.tbz" + checksum: "md5=c9ba2c738d264c420c642f7bb1cf4a36" +} diff --git a/esy.lock/opam/uutf.1.0.1/opam b/esy.lock/opam/uutf.1.0.1/opam new file mode 100644 index 000000000..e0d3ea812 --- /dev/null +++ b/esy.lock/opam/uutf.1.0.1/opam @@ -0,0 +1,38 @@ +opam-version: "2.0" +maintainer: "Daniel Bünzli " +authors: ["Daniel Bünzli "] +homepage: "http://erratique.ch/software/uutf" +doc: "http://erratique.ch/software/uutf/doc/Uutf" +dev-repo: "git+http://erratique.ch/repos/uutf.git" +bug-reports: "https://github.com/dbuenzli/uutf/issues" +tags: [ "unicode" "text" "utf-8" "utf-16" "codec" "org:erratique" ] +license: "ISC" +depends: [ + "ocaml" {>= "4.01.0"} + "ocamlfind" {build} + "ocamlbuild" {build} + "topkg" {build} + "uchar" +] +depopts: ["cmdliner"] +conflicts: ["cmdliner" { < "0.9.6"} ] +build: [[ + "ocaml" "pkg/pkg.ml" "build" + "--pinned" "%{pinned}%" + "--with-cmdliner" "%{cmdliner:installed}%" ]] +synopsis: "Non-blocking streaming Unicode codec for OCaml" +description: """ +Uutf is a non-blocking streaming codec to decode and encode the UTF-8, +UTF-16, UTF-16LE and UTF-16BE encoding schemes. It can efficiently +work character by character without blocking on IO. Decoders perform +character position tracking and support newline normalization. + +Functions are also provided to fold over the characters of UTF encoded +OCaml string values and to directly encode characters in OCaml +Buffer.t values. + +Uutf has no dependency and is distributed under the ISC license.""" +url { + src: "http://erratique.ch/software/uutf/releases/uutf-1.0.1.tbz" + checksum: "md5=b8535f974027357094c5cdb4bf03a21b" +} diff --git a/executable/Env.re b/executable/Env.re new file mode 100644 index 000000000..057138597 --- /dev/null +++ b/executable/Env.re @@ -0,0 +1,9 @@ +open Nsw; + +let run = () => { + Console.log( + Printf.sprintf("export PATH=%s/bin:$PATH", Directories.currentVersion), + ); + + Lwt.return(); +}; diff --git a/executable/Install.re b/executable/Install.re new file mode 100644 index 000000000..c5a090a54 --- /dev/null +++ b/executable/Install.re @@ -0,0 +1,101 @@ +open Nsw; + +let mkDownloadsDir = () => { + let exists = Lwt_unix.file_exists(Directories.downloads); + if%lwt (exists |> Lwt.map(x => !x)) { + Console.log( + + "Creating " + Directories.downloads + " for the first time" + , + ); + let%lwt _ = System.mkdirp(Directories.downloads); + Lwt.return(); + } else { + Lwt.return(); + }; +}; + +let main = (~version as versionName) => { + let%lwt os = System.NodeOS.get() + and arch = System.NodeArch.get() + and versionName = + switch (versionName) { + | Some(versionName) => Lwt.return(versionName) + | None => Nvmrc.getVersion() + }; + + let versionName = Versions.format(versionName); + + Console.log( + + "Looking for node " + versionName + " for " + + {System.NodeOS.toString(os)} + " " + {System.NodeArch.toString(arch)} + + , + ); + + let%lwt filepath = + Versions.getFileToDownload(~version=versionName, ~os, ~arch); + let tarDestination = + Filename.concat(Directories.downloads, versionName ++ ".tar.gz"); + + Console.log( + + "Downloading " + filepath + " to " + tarDestination + , + ); + + let%lwt _ = System.mkdirp(Filename.dirname(tarDestination)); + let%lwt _ = Http.download(filepath, ~into=tarDestination); + let extractionDestination = + Filename.concat(Directories.nodeVersions, versionName); + + Console.log( + + "Extracting " + tarDestination + " to " + extractionDestination + , + ); + + let%lwt _ = + Compression.extractFile(tarDestination, ~into=extractionDestination); + + Lwt.return(); +}; + +let run = (~version) => + try%lwt (main(~version)) { + | Versions.No_Download_For_System(os, arch) => + Console.log( + + "Version exists, but can't find a file for your system:\n" + " OS: " + {System.NodeOS.toString(os)} + "\n" + " Architecture: " + {System.NodeArch.toString(arch)} + , + ) + |> Lwt.return + | Versions.Version_not_found(version) => + Console.log( + + "Version " + version + " not found!" + , + ) + |> Lwt.return + }; \ No newline at end of file diff --git a/executable/ListInstallations.re b/executable/ListInstallations.re index 97b211cd7..15558f96b 100644 --- a/executable/ListInstallations.re +++ b/executable/ListInstallations.re @@ -1,122 +1,37 @@ -module Path = { - let rec join = xs => - switch (xs) { - | [x] => x - | [x, ...xs] => Filename.concat(x, join(xs)) - | [] => "" - }; -}; - -module Fs = { - open Core; - let readdir = dir => - switch (Sys.readdir(dir)) { - | x => Ok(x) - | exception (Sys_error(error)) => Error(error) - }; - - let realpath = Filename.realpath; -}; - -module Result = { - let return = x => Ok(x); - - let both = (a, b) => - switch (a, b) { - | (Error(_) as e, _) - | (_, Error(_) as e) => e - | (Ok(ax), Ok(bx)) => Ok((ax, bx)) - }; - - let map = (fn, res) => - switch (res) { - | Ok(x) => Ok(fn(x)) - | Error(_) as e => e - }; - - let bind = (fn, res) => - switch (res) { - | Ok(x) => fn(x) - | Error(_) as e => e - }; - - let fold = (error, ok, res) => - switch (res) { - | Ok(x) => ok(x) - | Error(x) => error(x) - }; - - module Let_syntax = { - let map = (x, ~f) => map(f, x); - let bind = (x, ~f) => bind(f, x); - }; -}; - -module Opt = { - let orThrow = (message, opt) => - switch (opt) { - | None => failwith(message) - | Some(x) => x - }; - - let fold = (none, some, opt) => - switch (opt) { - | None => none() - | Some(x) => some(x) - }; - - let toResult = (error, opt) => - switch (opt) { - | None => Error(error) - | Some(x) => Ok(x) - }; -}; - -module Directories = { - open Core; - let home = - Sys.getenv("HOME") - |> Opt.orThrow("There isn't $HOME environment variable set."); - let sfwRoot = Path.join([home, ".nsw"]); - let nodeVersions = Path.join([sfwRoot, "node-versions"]); - let currentVersion = Path.join([sfwRoot, "current"]); -}; - -let currentVersion = () => - switch (Fs.realpath(Directories.currentVersion)) { - | x => Some(x) - | exception (Unix.Unix_error(_, _, _)) => None - }; - -let printableVersions = (~current, ~versions) => { - open Pastel; +open Nsw; +let colorizeVersions = (~current, ~versions) => { let strings = versions |> List.map(version => { - let fullPath = Path.join([Directories.nodeVersions, version]); - let str = "- " ++ version; - fullPath == current ? str : str; + open Versions.Local; + let str = "- " ++ version.name; + + let color = + current + |> Opt.bind(current => + current.name == version.name ? Some(Pastel.Green) : None + ); + + str ; }); - ...strings ; + + "## List of installed versions:\n" + ...strings + ; }; -let run = () => { - open Result; +let getVersionsString = () => + Result.( + { + let%bind versions = + Versions.getInstalledVersions() |> Result.map(Array.to_list); - let%bind current = - currentVersion() - |> Opt.toResult("No version selected") - |> Result.fold(x => x, x => x) - |> Result.return; - let%bind x = Fs.readdir(Directories.nodeVersions); - let%bind versions = - Fs.readdir(Directories.nodeVersions) |> Result.map(Array.to_list); + let current = Versions.getCurrentVersion(); - Console.log( - "## List of installed versions:" , + colorizeVersions(~current, ~versions) |> Result.return; + } ); - printableVersions(~current, ~versions) |> Console.log; - Result.return(); -}; \ No newline at end of file + +let run = () => getVersionsString() |> Result.map(Console.log) |> Lwt.return; diff --git a/executable/ListLocal.re b/executable/ListLocal.re new file mode 100644 index 000000000..1d95c10f3 --- /dev/null +++ b/executable/ListLocal.re @@ -0,0 +1,24 @@ +open Nsw; + +let run = () => + Versions.Local.( + { + let%lwt versions = Versions.getInstalledVersions() |> Result.toLwt; + let currentVersion = Versions.getCurrentVersion(); + + Console.log("The following versions are installed:"); + + versions + |> Array.iter(version => { + let color = + switch (currentVersion) { + | None => None + | Some(x) when x.name == version.name => Some(Pastel.Cyan) + | Some(_) => None + }; + Console.log( "* " {version.name} ); + }); + + Lwt.return(); + } + ); diff --git a/executable/ListRemote.re b/executable/ListRemote.re new file mode 100644 index 000000000..4b013c64e --- /dev/null +++ b/executable/ListRemote.re @@ -0,0 +1,26 @@ +open Nsw; + +let run = () => { + Console.log("Looking for some node versions upstream..."); + + let%lwt versions = Versions.getRemoteVersions(); + let currentVersion = Versions.getCurrentVersion(); + + versions + |> List.iter(version => { + open Versions.Remote; + let str = "* " ++ version.name; + let color = + switch (currentVersion, version.installed) { + | (Some({name: currentVersionName, _}), _) + when currentVersionName == version.name => + Some(Pastel.Cyan) + | (_, true) => Some(Pastel.Green) + | (_, false) => None + }; + + Console.log( str ); + }); + + Lwt.return(); +}; diff --git a/executable/NswApp.re b/executable/NswApp.re index c94b841f6..b883bf517 100644 --- a/executable/NswApp.re +++ b/executable/NswApp.re @@ -1 +1,99 @@ -ListInstallations.run(); \ No newline at end of file +let version = "1.0.0"; + +module Commands = { + let use = version => Lwt_main.run(Use.run(version)); + let listRemote = () => Lwt_main.run(ListRemote.run()); + let listLocal = () => Lwt_main.run(ListLocal.run()); + let install = version => Lwt_main.run(Install.run(~version)); + let env = () => Lwt_main.run(Env.run()); +}; + +open Cmdliner; + +let help_secs = [ + `S(Manpage.s_common_options), + `P("These options are common to all commands."), + `S("MORE HELP"), + `P("Use `$(mname) $(i,COMMAND) --help' for help on a single command."), + `Noblank, + `S(Manpage.s_bugs), + `P("File bug reports at https://github.com/Schniz/nsw"), +]; + +let install = { + let doc = "Install another node version"; + let man = []; + + let selectedVersion = { + let doc = "Install another version specified in $(docv)."; + Arg.( + value & pos(0, some(string), None) & info([], ~docv="VERSION", ~doc) + ); + }; + + ( + Term.(const(Commands.install) $ selectedVersion), + Term.info("install", ~version, ~doc, ~exits=Term.default_exits, ~man), + ); +}; + +let listLocal = { + let doc = "List all the installed versions"; + let man = []; + + ( + Term.(app(const(Commands.listLocal), const())), + Term.info("ls", ~version, ~doc, ~exits=Term.default_exits, ~man), + ); +}; + +let listRemote = { + let doc = "List all the versions upstream"; + let man = []; + + ( + Term.(app(const(Commands.listRemote), const())), + Term.info("ls-remote", ~version, ~doc, ~exits=Term.default_exits, ~man), + ); +}; + +let use = { + let doc = "Switch to another installed node version"; + let man = []; + + let selectedVersion = { + let doc = "Switch to version $(docv).\nLeave empty to look for value from `.nvmrc`"; + Arg.( + value & pos(0, some(string), None) & info([], ~docv="VERSION", ~doc) + ); + }; + + ( + Term.(const(Commands.use) $ selectedVersion), + Term.info("use", ~version, ~doc, ~exits=Term.default_exits, ~man), + ); +}; + +let env = { + let doc = "Show env configurations"; + let sdocs = Manpage.s_common_options; + let man = help_secs; + ( + Term.(const(Commands.env) $ const()), + Term.info("env", ~version, ~doc, ~exits=Term.default_exits, ~man, ~sdocs), + ); +}; + +let defaultCmd = { + let doc = "Manage Node.js installations"; + let sdocs = Manpage.s_common_options; + let man = help_secs; + ( + Term.(ret(const(_ => `Help((`Pager, None))) $ const())), + Term.info("nsw", ~version, ~doc, ~exits=Term.default_exits, ~man, ~sdocs), + ); +}; + +let _ = + Term.eval_choice(defaultCmd, [install, use, listLocal, listRemote, env]) + |> Term.exit; diff --git a/executable/Use.re b/executable/Use.re new file mode 100644 index 000000000..df11b2ec9 --- /dev/null +++ b/executable/Use.re @@ -0,0 +1,63 @@ +open Nsw; + +let lwtIgnore = lwt => Lwt.catch(() => lwt, _ => Lwt.return()); + +exception Version_Not_Installed(string); + +let switchVersion = version => { + let versionDir = Filename.concat(Directories.nodeVersions, version); + + let%lwt _ = + if%lwt (Lwt_unix.file_exists(versionDir) |> Lwt.map(x => !x)) { + Lwt.fail(Version_Not_Installed(version)); + }; + + let destination = Filename.concat(versionDir, "installation"); + let source = Directories.currentVersion; + + Console.log( + + "Linking " + source + " to " + destination + , + ); + + let%lwt _ = Lwt_unix.unlink(Directories.currentVersion) |> lwtIgnore; + let%lwt _ = Lwt_unix.symlink(destination, Directories.currentVersion); + + Console.log( + "Using " version , + ); + + Lwt.return(); +}; + +let main = (~version as providedVersion) => { + let%lwt version = + switch (providedVersion) { + | Some(version) => Lwt.return(version) + | None => Nvmrc.getVersion() + }; + switchVersion(Versions.format(version)); +}; + +let run = version => + try%lwt (main(~version)) { + | Version_Not_Installed(version) => + Console.log( + + "The following version is not installed: " + version + , + ) + |> Lwt.return + | Nvmrc.Version_Not_Provided => + Console.log( + + "No .nvmrc was found in the current directory. Please provide a version number." + , + ) + |> Lwt.return + }; diff --git a/executable/dune b/executable/dune index a22c44806..a4234b3e4 100644 --- a/executable/dune +++ b/executable/dune @@ -5,8 +5,8 @@ (executable ; The entrypoint module (name NswApp) ; From package.json main field - ; The name of the executable (runnable via esy x NswApp.exe) - (public_name NswApp.exe) ; From package.json name field - (libraries core lwt lwt.unix console.lib pastel.lib nsw.lib ) ; From package.json require field (array of strings) + ; The name of the executable (runnable via esy x nsw.exe) + (public_name nsw.exe) ; From package.json name field + (libraries core cmdliner lwt lwt.unix lambdasoup console.lib pastel.lib nsw.lib ) ; From package.json require field (array of strings) (preprocess ( pps lwt_ppx ppx_let )) ; From package.json preprocess field ) \ No newline at end of file diff --git a/feature_tests/basic/run.sh b/feature_tests/basic/run.sh new file mode 100644 index 000000000..95b34e96f --- /dev/null +++ b/feature_tests/basic/run.sh @@ -0,0 +1,8 @@ +eval $(nsw env) +nsw install v8.11.3 +nsw use v8.11.3 + +if [ "$(node --version)" != "v8.11.3" ]; then + echo "Node version is not v8.11.3!" + exit 1 +fi \ No newline at end of file diff --git a/feature_tests/nvmrc/.nvmrc b/feature_tests/nvmrc/.nvmrc new file mode 100644 index 000000000..e3cbcda79 --- /dev/null +++ b/feature_tests/nvmrc/.nvmrc @@ -0,0 +1 @@ +10.9.0 \ No newline at end of file diff --git a/feature_tests/nvmrc/run.sh b/feature_tests/nvmrc/run.sh new file mode 100644 index 000000000..206a8adaf --- /dev/null +++ b/feature_tests/nvmrc/run.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +eval $(nsw env) +nsw install +nsw use + +if [ "$(node --version)" != "v10.9.0" ]; then + echo "Node version is not v10.9.0!" + exit 1 +fi \ No newline at end of file diff --git a/feature_tests/run.sh b/feature_tests/run.sh new file mode 100755 index 000000000..683f522ba --- /dev/null +++ b/feature_tests/run.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +set -e + +DIRECTORY=`dirname $0` +BINARY=$1 +TEMP_DIR_BASE=$(pwd)/$DIRECTORY/.tmp +TEMP_BINARY_PATH=$TEMP_DIR_BASE/bin +TEMP_NSW_DIR=$TEMP_DIR_BASE/.nsw + +if [ "$BINARY" == "" ]; then + echo "No binary supplied!" + exit 1 +fi + +echo "using nvm=$BINARY" + +rm -rf $TEMP_DIR_BASE +mkdir $TEMP_DIR_BASE $TEMP_BINARY_PATH +cp $BINARY $TEMP_BINARY_PATH/nsw + +for test_file in $DIRECTORY/*/run.sh; do + rm -rf $TEMP_NSW_DIR + + echo "Running test in $test_file" + echo "Running test in $test_file" | sed "s/./-/g" + (cd $(dirname $test_file) && NSW_DIR=$TEMP_NSW_DIR PATH=$TEMP_BINARY_PATH:$PATH bash $(basename $test_file)) + echo "" + echo " -> Finished!" + + rm -rf $TEMP_NSW_DIR +done diff --git a/library/Compression.re b/library/Compression.re new file mode 100644 index 000000000..e983e6361 --- /dev/null +++ b/library/Compression.re @@ -0,0 +1,15 @@ +let extractFile = (~into as destination, filepath) => { + let%lwt _ = System.mkdirp(destination); + let%lwt _ = + System.unix_exec( + "tar", + ~args=[|"-xvf", filepath, "--directory", destination|], + ~stderr=`Dev_null, + ); + let%lwt files = Fs.readdir(destination) |> Result.toLwt; + let filename = files[0]; + Lwt_unix.rename( + Filename.concat(destination, filename), + Filename.concat(destination, "installation"), + ); +}; diff --git a/library/Directories.re b/library/Directories.re new file mode 100644 index 000000000..179a695b2 --- /dev/null +++ b/library/Directories.re @@ -0,0 +1,13 @@ +let sfwRoot = + Opt.( + Sys.getenv_opt("NSW_DIR") + or { + let home = + Sys.getenv_opt("HOME") + |> Opt.orThrow("There isn't $HOME environment variable set."); + Filename.concat(home, ".nsw"); + } + ); +let nodeVersions = Filename.concat(sfwRoot, "node-versions"); +let currentVersion = Filename.concat(sfwRoot, "current"); +let downloads = Filename.concat(sfwRoot, "downloads"); \ No newline at end of file diff --git a/library/Fs.re b/library/Fs.re new file mode 100644 index 000000000..6be72ae3a --- /dev/null +++ b/library/Fs.re @@ -0,0 +1,19 @@ +open Core; + +let readdir = dir => + switch (Sys.readdir(dir)) { + | x => Ok(x) + | exception (Sys_error(error)) => Error(error) + }; + +let writeFile = (path, contents) => { + let%lwt x = Lwt_unix.openfile(path, [Unix.O_RDWR, Unix.O_CREAT], 777); + let%lwt _ = + Lwt.finalize( + () => Lwt_unix.write_string(x, contents, 0, String.length(contents)), + () => Lwt_unix.close(x), + ); + Lwt.return(); +}; + +let realpath = Filename.realpath; diff --git a/library/Http.re b/library/Http.re new file mode 100644 index 000000000..bba03dbe1 --- /dev/null +++ b/library/Http.re @@ -0,0 +1,53 @@ +type response = { + body: string, + status: int, +}; + +let body = response => response.body; +let status = response => response.status; + +let rec getBody = listOfStrings => { + switch (listOfStrings) { + | [] => "" + | ["", ...rest] => String.concat("\n", rest) + | [_, ...xs] => getBody(xs) + }; +}; + +let rec getStatus = string => { + List.nth(String.split_on_char(' ', string), 1); +}; + +exception Unknown_status_code(response); +exception Not_found(response); +exception Internal_server_error(response); + +let verifyStatus = response => { + switch (response.status) { + | 200 => Lwt.return(response) + | x when x / 100 == 4 => Lwt.fail(Not_found(response)) + | x when x / 100 == 5 => Lwt.fail(Internal_server_error(response)) + | x => Lwt.fail(Unknown_status_code(response)) + }; +}; + +let parseResponse = lines => { + let body = getBody(lines); + let status = getStatus(lines |> List.hd) |> int_of_string; + {body, status}; +}; + +let makeRequest = url => { + let%lwt response = + System.unix_exec("curl", ~args=[|url, "-D", "-", "--silent"|]); + response |> parseResponse |> verifyStatus; +}; + +let download = (url, ~into) => { + let%lwt response = + System.unix_exec( + "curl", + ~args=[|url, "-D", "-", "--silent", "-o", into|], + ); + response |> parseResponse |> verifyStatus; +}; \ No newline at end of file diff --git a/library/Nvmrc.re b/library/Nvmrc.re new file mode 100644 index 000000000..ff8c370f8 --- /dev/null +++ b/library/Nvmrc.re @@ -0,0 +1,14 @@ +exception Version_Not_Provided; + +let getVersion = () => { + let%lwt cwd = Lwt_unix.getcwd(); + let nvmrcFile = Filename.concat(cwd, ".nvmrc"); + try%lwt ( + Lwt_io.lines_of_file(nvmrcFile) + |> Lwt_stream.to_list + |> Lwt.map(List.hd) + |> Lwt.map(String.trim) + ) { + | Unix.Unix_error(Unix.ENOENT, _, _) => Lwt.fail(Version_Not_Provided) + }; +}; diff --git a/library/Opt.re b/library/Opt.re new file mode 100644 index 000000000..d2e5ebf47 --- /dev/null +++ b/library/Opt.re @@ -0,0 +1,33 @@ +let orThrow = (message, opt) => + switch (opt) { + | None => failwith(message) + | Some(x) => x + }; + +let map = (fn, opt) => + switch (opt) { + | None => None + | Some(x) => Some(fn(x)) + }; + +let bind = (fn, opt) => + switch (opt) { + | None => None + | Some(x) => fn(x) + }; + +let fold = (none, some, opt) => + switch (opt) { + | None => none() + | Some(x) => some(x) + }; + +let toResult = (error, opt) => + switch (opt) { + | None => Error(error) + | Some(x) => Ok(x) + }; + +let some = x => Some(x); + +let (or) = (opt, b) => fold(() => b, x => x, opt); diff --git a/library/Result.re b/library/Result.re new file mode 100644 index 000000000..3df533f56 --- /dev/null +++ b/library/Result.re @@ -0,0 +1,43 @@ +let return = x => Ok(x); + +let both = (a, b) => + switch (a, b) { + | (Error(_) as e, _) + | (_, Error(_) as e) => e + | (Ok(ax), Ok(bx)) => Ok((ax, bx)) + }; + +let mapError = (fn, res) => + switch (res) { + | Error(x) => Error(fn(x)) + | Ok(_) as x => x + }; + +let map = (fn, res) => + switch (res) { + | Ok(x) => Ok(fn(x)) + | Error(_) as e => e + }; + +let bind = (fn, res) => + switch (res) { + | Ok(x) => fn(x) + | Error(_) as e => e + }; + +let fold = (error, ok, res) => + switch (res) { + | Ok(x) => ok(x) + | Error(x) => error(x) + }; + +module Let_syntax = { + let map = (x, ~f) => map(f, x); + let bind = (x, ~f) => bind(f, x); +}; + +let toLwt = res => + switch (res) { + | Error(x) => Lwt.fail_with(x) + | Ok(x) => Lwt.return(x) + }; diff --git a/library/System.re b/library/System.re new file mode 100644 index 000000000..925c553ad --- /dev/null +++ b/library/System.re @@ -0,0 +1,70 @@ +let unix_exec = + (~args=[||], ~env=?, ~stderr: Lwt_process.redirection=`Keep, command) => { + let realArgs = Array.append([|command|], args); + Lwt_process.pread_lines(~stderr, ~env?, ("", realArgs)) + |> Lwt_stream.to_list; +}; + +let mkdirp = destination => + unix_exec("mkdir", ~stderr=`Dev_null, ~args=[|"-p", destination|]); + +module NodeArch = { + type t = + | X32 + | X64 + | Other; + + let rec last = xs => + switch (xs) { + | [x] => Some(x) + | [_, ...xs] => last(xs) + | [] => None + }; + + let findArches = unameResult => { + let words = unameResult |> List.hd |> String.split_on_char(' '); + List.exists(word => word == "x86_64", words) ? X64 : X32; + }; + + /* Get node-compliant architecture (x64, x86) */ + let get = () => + switch (Sys.os_type) { + | "Unix" => + let%lwt result = unix_exec("uname", ~args=[|"-a"|]); + try (result |> findArches |> Lwt.return) { + | _ => Lwt.fail_with("Error getting unix information") + }; + | _ => Lwt.return(Other) + }; + + let toString = + fun + | X64 => "x64" + | X32 => "x32" + | Other => "other"; +}; + +module NodeOS = { + type t = + | Darwin + | Linux + | Other(string); + + let get = () => + switch (Sys.os_type) { + | "Unix" => + let%lwt result = unix_exec("uname", ~args=[|"-s"|]); + switch (result |> List.hd) { + | "Darwin" => Lwt.return(Darwin) + | _ => Lwt.return(Linux) + | exception _ => Lwt.fail_with("Error getting unix information") + }; + | other => Other(other) |> Lwt.return + }; + + let toString = + fun + | Darwin => "darwin" + | Linux => "linux" + | Other(_) => "other"; +}; diff --git a/library/Util.re b/library/Util.re deleted file mode 100644 index 0e7526842..000000000 --- a/library/Util.re +++ /dev/null @@ -1 +0,0 @@ -let foo = () => print_endline("Hello"); diff --git a/library/Versions.re b/library/Versions.re new file mode 100644 index 000000000..2954f32ad --- /dev/null +++ b/library/Versions.re @@ -0,0 +1,147 @@ +module VersionSet = Set.Make(String); + +module Local = { + type t = { + name: string, + fullPath: string, + }; +}; + +exception Version_not_found(string); + +module Remote = { + type t = { + name: string, + baseURL: string, + installed: bool, + }; + + let skip = (~amount, str) => + Str.last_chars(str, String.length(str) - amount); + + let parseSemver = version => version |> skip(~amount=1) |> Semver.of_string; + + let compare = (v1, v2) => + switch (parseSemver(v1), parseSemver(v2)) { + | (Some(v1), Some(v2)) => Semver.compare(v1, v2) + | (None, _) + | (_, None) => - Core.String.compare(v1, v2) + }; + + let getInstalledVersionSet = () => + Fs.readdir(Directories.nodeVersions) + |> Result.fold(_ => [||], x => x) + |> Array.fold_left( + (acc, curr) => VersionSet.add(curr, acc), + VersionSet.empty, + ); + + let getRelativeLinksFromHTML = html => + Soup.parse(html) + |> Soup.select("pre a") + |> Soup.to_list + |> List.map(Soup.attribute("href")) + |> Core.List.filter_map(~f=x => x); + + let downloadFileSuffix = ".tar.gz"; + + let getVersionFromFilename = filename => { + let strings = filename |> String.split_on_char('-'); + List.nth(strings, 1); + }; +}; + +let format = version => { + let version = + switch (Str.first_chars(version, 1) |> Int32.of_string) { + | _ => "v" ++ version + | exception _ => version + }; + + version; +}; + +let endsWith = (~suffix, str) => { + let suffixLength = String.length(suffix); + + String.length(str) > suffixLength + && Str.last_chars(str, suffixLength) == suffix; +}; + +exception No_Download_For_System(System.NodeOS.t, System.NodeArch.t); + +let getFileToDownload = (~version as versionName, ~os, ~arch) => { + let versionName = + switch (Str.first_chars(versionName, 1) |> Int32.of_string) { + | _ => "v" ++ versionName + | exception _ => versionName + }; + let url = "https://nodejs.org/dist/" ++ versionName ++ "/"; + let%lwt html = + try%lwt (Http.makeRequest(url) |> Lwt.map(Http.body)) { + | Http.Not_found(_) => Lwt.fail(Version_not_found(versionName)) + }; + let filenames = + html + |> Remote.getRelativeLinksFromHTML + |> List.filter( + endsWith( + ~suffix= + System.NodeOS.toString(os) + ++ "-" + ++ System.NodeArch.toString(arch) + ++ Remote.downloadFileSuffix, + ), + ); + + switch (filenames |> List.hd) { + | x => Lwt.return(url ++ x) + | exception _ => Lwt.fail(No_Download_For_System(os, arch)) + }; +}; + +let getCurrentVersion = () => + switch (Fs.realpath(Directories.currentVersion)) { + | installationPath => + let fullPath = Filename.dirname(installationPath); + Some(Local.{fullPath, name: Core.Filename.basename(fullPath)}); + | exception (Unix.Unix_error(_, _, _)) => None + }; + +let getInstalledVersions = () => + Fs.readdir(Directories.nodeVersions) + |> Result.map(x => { + Array.sort(Remote.compare, x); + x; + }) + |> Result.map( + Array.map(name => + Local.{ + name, + fullPath: Filename.concat(Directories.nodeVersions, name), + } + ), + ); + +let getRemoteVersions = () => { + let%lwt bodyString = + Http.makeRequest("https://nodejs.org/dist/") |> Lwt.map(Http.body); + + let versions = bodyString |> Remote.getRelativeLinksFromHTML; + let installedVersions = Remote.getInstalledVersionSet(); + + versions + |> Core.List.filter(~f=x => + Str.last_chars(x, 1) == "/" && Str.first_chars(x, 1) != "." + ) + |> Core.List.map(~f=x => Str.first_chars(x, String.length(x) - 1)) + |> List.sort(Remote.compare) + |> List.map(name => + Remote.{ + name, + installed: VersionSet.find_opt(name, installedVersions) != None, + baseURL: "https://nodejs.org/dist/" ++ name ++ "/", + } + ) + |> Lwt.return; +}; \ No newline at end of file diff --git a/library/dune b/library/dune index 30a4e791b..ac7c1d677 100644 --- a/library/dune +++ b/library/dune @@ -7,4 +7,6 @@ (name Nsw) ; Other libraries list this name in their package.json 'require' field to use this library. (public_name nsw.lib) + (libraries str core lwt lwt.unix lambdasoup semver ) + (preprocess ( pps lwt_ppx ppx_let )) ; From package.json preprocess field ) \ No newline at end of file diff --git a/package.json b/package.json index 1515a7e7a..a4c814f46 100644 --- a/package.json +++ b/package.json @@ -4,42 +4,51 @@ "description": "My Project", "esy": { "build": "pesy", + "buildsInSource": "_build", "release": { "releasedBinaries": [ - "NswApp.exe" + "nsw.exe" ] } }, "buildDirs": { "test": { - "require": ["nsw.lib"], + "require": ["nsw.lib", "rely.lib"], "main": "TestNsw", - "name": "TestNsw.exe" + "name": "TestNsw.exe", + "ocamloptFlags": ["-linkall", "-g"] }, "library": { + "preprocess": ["pps", "lwt_ppx", "ppx_let"], + "require": ["str", "core", "lwt", "lwt.unix", "lambdasoup", "semver"], "name": "nsw.lib", "namespace": "Nsw" }, "executable": { "preprocess": ["pps", "lwt_ppx", "ppx_let"], - "require": ["core", "lwt", "lwt.unix", "console.lib", "pastel.lib", "nsw.lib"], + "require": ["core", "cmdliner", "lwt", "lwt.unix", "lambdasoup", "console.lib", "pastel.lib", "nsw.lib"], "main": "NswApp", - "name": "NswApp.exe" + "name": "nsw.exe" } }, "scripts": { "pesy": "bash -c 'env PESY_MODE=update pesy'", - "test": "esy x TestNsw.exe" + "test": "esy x TestNsw.exe", + "fmt": "bash -c 'refmt --in-place {library,executable,test}/*.re'" }, "dependencies": { "@opam/dune": "*", + "@opam/semver": "*", "@opam/core": "*", + "@opam/cmdliner": "*", "@opam/lwt": "*", "@opam/lwt_ppx": "*", "@opam/ppx_let": "*", "@reason-native/console": "*", "@reason-native/pastel": "*", + "@reason-native/rely": "*", "@esy-ocaml/reason": "*", + "@opam/lambdasoup": "*", "refmterr": "*", "ocaml": "~4.6.0", "pesy": "*" diff --git a/test/SmokeTest.re b/test/SmokeTest.re new file mode 100644 index 000000000..cbb57fd06 --- /dev/null +++ b/test/SmokeTest.re @@ -0,0 +1,17 @@ +open TestFramework; + +describe("Smoke test", ({test}) => { + test("Tests run!", ({expect}) => + expect.int(1).toBe(1) + ); + + test("Get version", ({expect}) => { + let version = run([|"--version"|]); + expect.string(version).toMatch("^[0-9]+.[0-9]+.[0-9]+$"); + }); + + test("env", ({expect}) => { + let env = run([|"env"|]) |> redactSfwRoot; + expect.string(env).toMatchSnapshot(); + }); +}); \ No newline at end of file diff --git a/test/TestFramework.re b/test/TestFramework.re new file mode 100644 index 000000000..d72f7f598 --- /dev/null +++ b/test/TestFramework.re @@ -0,0 +1,31 @@ +let projectDir = Sys.getcwd(); +let tmpDir = Filename.concat(projectDir, ".nswTmp"); + +include Rely.Make({ + let config = + Rely.TestFrameworkConfig.initialize({ + snapshotDir: + Filename.concat( + projectDir, + Filename.concat("test", "__snapshots__"), + ), + projectDir, + }); +}); + +let run = args => { + let arguments = + args |> Array.append([|"./_build/default/executable/NswApp.exe"|]); + let env = Unix.environment() |> Array.append([|"NSW_DIR=" ++ tmpDir|]); + let result = + Lwt_process.pread_chars(~env, ("", arguments)) |> Lwt_stream.to_string; + Lwt_main.run(result); +}; + +let clearTmpDir = () => { + let _ = Lwt_process.pread(("", [|"rm", "-rf", tmpDir|])) |> Lwt_main.run; + (); +}; + +let redactSfwRoot = + Str.global_replace(Str.regexp_string(tmpDir), ""); \ No newline at end of file diff --git a/test/TestNsw.re b/test/TestNsw.re index bc88c6a54..9c5860749 100644 --- a/test/TestNsw.re +++ b/test/TestNsw.re @@ -1,2 +1,3 @@ -Nsw.Util.foo(); -print_endline("Add Your Test Cases Here"); +include SmokeTest; + +TestFramework.cli(); \ No newline at end of file diff --git a/test/__snapshots__/Smoke_test.4d362c3c.0.snapshot b/test/__snapshots__/Smoke_test.4d362c3c.0.snapshot new file mode 100644 index 000000000..4156fabd0 --- /dev/null +++ b/test/__snapshots__/Smoke_test.4d362c3c.0.snapshot @@ -0,0 +1,3 @@ +Smoke test › env +export PATH=/current/bin:$PATH + diff --git a/test/dune b/test/dune index 209103ebf..331f7cf68 100644 --- a/test/dune +++ b/test/dune @@ -7,5 +7,6 @@ (name TestNsw) ; From package.json main field ; The name of the executable (runnable via esy x TestNsw.exe) (public_name TestNsw.exe) ; From package.json name field - (libraries nsw.lib ) ; From package.json require field (array of strings) + (libraries nsw.lib rely.lib ) ; From package.json require field (array of strings) + (ocamlopt_flags ( -linkall -g )) ; From package.json ocamloptFlags field ) \ No newline at end of file