diff --git a/.circleci/config.yml b/.circleci/config.yml index a7cb1471..d41b04e5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,6 +12,7 @@ jobs: environment: LEIN_ROOT: "true" BABASHKA_PLATFORM: linux # could be used in jar name + resource_class: large steps: - checkout - run: @@ -67,9 +68,11 @@ jobs: working_directory: ~/repo environment: LEIN_ROOT: "true" - GRAALVM_HOME: /home/circleci/graalvm-ce-java8-19.3.0 + GRAALVM_HOME: /home/circleci/graalvm-ce-java8-19.3.1 BABASHKA_PLATFORM: linux # used in release script BABASHKA_TEST_ENV: native + BABASHKA_XMX: "-J-Xmx7g" + resource_class: large steps: - checkout - run: @@ -99,14 +102,10 @@ jobs: name: Download GraalVM command: | cd ~ - if ! [ -d graalvm-ce-java8-19.3.0 ]; then - curl -O -sL https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-19.3.0/graalvm-ce-java8-linux-amd64-19.3.0.tar.gz - tar xzf graalvm-ce-java8-linux-amd64-19.3.0.tar.gz + if ! [ -d graalvm-ce-java8-19.3.1 ]; then + curl -O -sL https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-19.3.1/graalvm-ce-java8-linux-amd64-19.3.1.tar.gz + tar xzf graalvm-ce-java8-linux-amd64-19.3.1.tar.gz fi - # - run: - # name: Install GraalVM SSL libs - # command: | - # .circleci/script/graalvm_ssl - run: name: Build binary command: | @@ -117,10 +116,6 @@ jobs: command: | script/test script/run_lib_tests - # - run: - # name: Performance report - # command: | - # .circleci/script/performance - run: name: Release command: | @@ -128,7 +123,78 @@ jobs: - save_cache: paths: - ~/.m2 - - ~/graalvm-ce-java8-19.3.0 + - ~/graalvm-ce-java8-19.3.1 + key: linux-{{ checksum "project.clj" }}-{{ checksum ".circleci/config.yml" }} + - store_artifacts: + path: /tmp/release + destination: release + - run: + name: Publish artifact link to Slack + command: | + ./bb .circleci/script/publish_artifact.clj + linux-static: + docker: + - image: circleci/clojure:lein-2.8.1 + working_directory: ~/repo + environment: + LEIN_ROOT: "true" + GRAALVM_HOME: /home/circleci/graalvm-ce-java8-19.3.1 + BABASHKA_PLATFORM: linux-static # used in release script + BABASHKA_TEST_ENV: native + BABASHKA_STATIC: true + BABASHKA_XMX: "-J-Xmx7g" + resource_class: large + steps: + - checkout + - run: + name: "Pull Submodules" + command: | + git submodule init + git submodule update + - restore_cache: + keys: + - linux-{{ checksum "project.clj" }}-{{ checksum ".circleci/config.yml" }} + - run: + name: Install Clojure + command: | + wget https://download.clojure.org/install/linux-install-1.10.1.447.sh + chmod +x linux-install-1.10.1.447.sh + sudo ./linux-install-1.10.1.447.sh + - run: + name: Install lsof + command: | + sudo apt-get install lsof + - run: + name: Install native dev tools + command: | + sudo apt-get update + sudo apt-get -y install gcc g++ zlib1g-dev + - run: + name: Download GraalVM + command: | + cd ~ + if ! [ -d graalvm-ce-java8-19.3.1 ]; then + curl -O -sL https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-19.3.1/graalvm-ce-java8-linux-amd64-19.3.1.tar.gz + tar xzf graalvm-ce-java8-linux-amd64-19.3.1.tar.gz + fi + - run: + name: Build binary + command: | + script/compile + no_output_timeout: 30m + - run: + name: Run tests + command: | + script/test + script/run_lib_tests + - run: + name: Release + command: | + .circleci/script/release + - save_cache: + paths: + - ~/.m2 + - ~/graalvm-ce-java8-19.3.1 key: linux-{{ checksum "project.clj" }}-{{ checksum ".circleci/config.yml" }} - store_artifacts: path: /tmp/release @@ -141,9 +207,11 @@ jobs: macos: xcode: "9.0" environment: - GRAALVM_HOME: /Users/distiller/graalvm-ce-java8-19.3.0/Contents/Home + GRAALVM_HOME: /Users/distiller/graalvm-ce-java8-19.3.1/Contents/Home BABASHKA_PLATFORM: macos # used in release script BABASHKA_TEST_ENV: native + BABASHKA_XMX: "-J-Xmx7g" + resource_class: large steps: - checkout - run: @@ -157,24 +225,20 @@ jobs: - run: name: Install Clojure command: | - .circleci/script/install-clojure /usr/local + script/install-clojure /usr/local - run: name: Install Leiningen command: | - .circleci/script/install-leiningen + script/install-leiningen - run: name: Download GraalVM command: | cd ~ ls -la - if ! [ -d graalvm-ce-java8-19.3.0 ]; then - curl -O -sL https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-19.3.0/graalvm-ce-java8-darwin-amd64-19.3.0.tar.gz - tar xzf graalvm-ce-java8-darwin-amd64-19.3.0.tar.gz + if ! [ -d graalvm-ce-java8-19.3.1 ]; then + curl -O -sL https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-19.3.1/graalvm-ce-java8-darwin-amd64-19.3.1.tar.gz + tar xzf graalvm-ce-java8-darwin-amd64-19.3.1.tar.gz fi - # - run: - # name: Install GraalVM SSL libs - # command: | - # .circleci/script/graalvm_ssl - run: name: Build binary command: | @@ -185,10 +249,6 @@ jobs: command: | script/test script/run_lib_tests - # - run: - # name: Performance report - # command: | - # .circleci/script/performance - run: name: Release command: | @@ -196,7 +256,7 @@ jobs: - save_cache: paths: - ~/.m2 - - ~/graalvm-ce-java8-19.3.0 + - ~/graalvm-ce-java8-19.3.1 key: mac-{{ checksum "project.clj" }}-{{ checksum ".circleci/config.yml" }} - store_artifacts: path: /tmp/release @@ -206,6 +266,7 @@ jobs: command: | ./bb .circleci/script/publish_artifact.clj deploy: + resource_class: large docker: - image: circleci/clojure:lein-2.8.1 working_directory: ~/repo @@ -229,6 +290,7 @@ jobs: - ~/.m2 key: v1-dependencies-{{ checksum "project.clj" }} docker: + resource_class: large docker: - image: circleci/buildpack-deps:stretch steps: @@ -249,6 +311,7 @@ workflows: jobs: - jvm - linux + - linux-static - mac - deploy: filters: @@ -257,6 +320,7 @@ workflows: requires: - jvm - linux + - linux-static - mac - docker: filters: @@ -265,4 +329,5 @@ workflows: requires: - jvm - linux + - linux-static - mac diff --git a/.circleci/script/docker b/.circleci/script/docker index d2bda10e..d7410e0a 100755 --- a/.circleci/script/docker +++ b/.circleci/script/docker @@ -17,7 +17,7 @@ fi if [ -z "$CIRCLE_PULL_REQUEST" ] && [ "$CIRCLE_BRANCH" = "master" ]; then echo "Building Docker image $image_name:$image_tag" echo "$DOCKERHUB_PASS" | docker login -u "$DOCKERHUB_USER" --password-stdin - docker build -t "$image_name" . + docker build -t "$image_name" --build-arg BABASHKA_XMX="-J-Xmx6900m" . docker tag "$image_name:$latest_tag" "$image_name:$image_tag" # we only update latest when it's not a SNAPSHOT version if [ "false" = "$snapshot" ]; then diff --git a/.circleci/script/install-leiningen b/.circleci/script/install-leiningen deleted file mode 100755 index ff5f8235..00000000 --- a/.circleci/script/install-leiningen +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -curl https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein > lein -sudo mkdir -p /usr/local/bin/ -sudo mv lein /usr/local/bin/lein -sudo chmod a+x /usr/local/bin/lein diff --git a/.circleci/script/publish_artifact.clj b/.circleci/script/publish_artifact.clj index 1b5f5b74..6b0a75e9 100755 --- a/.circleci/script/publish_artifact.clj +++ b/.circleci/script/publish_artifact.clj @@ -1,23 +1,35 @@ -(require '[clojure.java.shell :refer [sh]] +(require '[cheshire.core :refer [generate-string]] '[clojure.java.io :as io] - '[cheshire.core :refer [generate-string]] + '[clojure.java.shell :refer [sh]] '[clojure.string :as str]) (def channel "#babashka_circleci_builds") #_(def channel "#_test") (def babashka-version (str/trim (slurp (io/file "resources" "BABASHKA_VERSION")))) - -(def text (format "[%s - %s@%s]: https://%s-201467090-gh.circle-artifacts.com/0/release/babashka-%s-%s-amd64.zip" - (System/getenv "BABASHKA_PLATFORM") - (System/getenv "CIRCLE_BRANCH") - (System/getenv "CIRCLE_SHA1") - (System/getenv "CIRCLE_BUILD_NUM") - babashka-version - (System/getenv "BABASHKA_PLATFORM"))) - (def slack-hook-url (System/getenv "SLACK_HOOK_URL")) -(when slack-hook-url - (let [json (generate-string {:username "borkdude" - :channel channel - :text text})] - (sh "curl" "-X" "POST" "-H" "Content-Type: application/json" "-d" json slack-hook-url))) + +(defn slack! [text] + (when slack-hook-url + (let [json (generate-string {:username "borkdude" + :channel channel + :text text})] + (sh "curl" "-X" "POST" "-H" "Content-Type: application/json" "-d" json slack-hook-url)))) + +(def release-text (format "[%s - %s@%s]: https://%s-201467090-gh.circle-artifacts.com/0/release/babashka-%s-%s-amd64.zip" + (System/getenv "BABASHKA_PLATFORM") + (System/getenv "CIRCLE_BRANCH") + (System/getenv "CIRCLE_SHA1") + (System/getenv "CIRCLE_BUILD_NUM") + babashka-version + (System/getenv "BABASHKA_PLATFORM"))) + +(slack! release-text) + +(def binary-size-text + (format "[%s - %s@%s] binary size: %s" + (System/getenv "BABASHKA_PLATFORM") + (System/getenv "CIRCLE_BRANCH") + (System/getenv "CIRCLE_SHA1") + (slurp (io/file "/tmp/bb_size/size")))) + +(slack! binary-size-text) diff --git a/.circleci/script/release b/.circleci/script/release index 2d32f514..5e6325bf 100755 --- a/.circleci/script/release +++ b/.circleci/script/release @@ -8,6 +8,8 @@ cp bb /tmp/release VERSION=$(cat resources/BABASHKA_VERSION) cd /tmp/release +mkdir -p /tmp/bb_size +./bb '(spit "/tmp/bb_size/size" (.length (io/file "bb")))' ## release binary as zip archive diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index e4db19e4..cba99996 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -4,4 +4,4 @@ babashka.impl.File/gen-wrapper-fn-2 clojure.core/def babashka.impl.Pattern/gen-wrapper-fn-2 clojure.core/def babashka.impl.Pattern/gen-constants clojure.core/declare} - :linters {:unsorted-namespaces {:level :warning}}} + :linters {:unsorted-required-namespaces {:level :warning}}} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..eefa27e4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +.circleci/ +.git/ +.clj-kondo/ +.github/ +doc/ +examples/ +logo/ +test-resources/ +test/ +.gitignore +.carve_ignore +.gitmodules +appveyor.yml +CHANGES.md +deps.edn +Dockerfile +LICENSE +README.md diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index c7fbd994..2ff623c6 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -2,7 +2,7 @@ github: borkdude # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: borkdude -open_collective: # Replace with a single Open Collective username +open_collective: babashka ko_fi: borkdude tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry diff --git a/.github/script/deploy b/.github/script/deploy new file mode 100755 index 00000000..31c8a147 --- /dev/null +++ b/.github/script/deploy @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +if [ -z "$GITHUB_HEAD_REF" ] && [ "${GITHUB_REF##*/}" = "master" ] +then + lein deploy clojars +fi + +exit 0; diff --git a/.github/script/docker b/.github/script/docker new file mode 100755 index 00000000..a0121d94 --- /dev/null +++ b/.github/script/docker @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +set -eo pipefail + +image_name="borkdude/babashka" +image_tag=$(cat resources/BABASHKA_VERSION) +latest_tag="latest" + +if [[ $image_tag =~ SNAPSHOT$ ]] +then + echo "This is a snapshot version" + snapshot="true" +else + echo "This is a non-snapshot version" + snapshot="false" +fi + +if [ -z "$GITHUB_HEAD_REF" ] && [ "${GITHUB_REF##*/}" = "master" ] +then + echo "Building Docker image $image_name:$image_tag" + echo "$DOCKERHUB_PASS" | docker login -u "$DOCKERHUB_USER" --password-stdin + docker build -t "$image_name" . + docker tag "$image_name:$latest_tag" "$image_name:$image_tag" + # we only update latest when it's not a SNAPSHOT version + if [ "false" = "$snapshot" ]; then + echo "Pushing image $image_name:$latest_tag" + docker push "$image_name:$latest_tag" + fi + # we update the version tag, even if it's a SNAPSHOT version + echo "Pushing image $image_name:$image_tag" + docker push "$image_name:$image_tag" +else + echo "Not publishing Docker image" +fi + +exit 0; diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..ae444cf3 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,345 @@ +name: build + +on: [push + , pull_request + ] + +jobs: + + scratch: + runs-on: ubuntu-18.04 + steps: + - name: Git checkout + uses: actions/checkout@v1 + with: + fetch-depth: 1 + submodules: 'true' + + - name: Scratch + run: | + echo "Scratch" + + jvm: + # ubuntu 18.04 comes with lein + java8 installed + runs-on: ubuntu-18.04 + steps: + - name: Git checkout + uses: actions/checkout@v1 + with: + fetch-depth: 1 + submodules: 'true' + + - name: Cache deps + uses: actions/cache@v1 + id: cache-deps + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('project.clj') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Fetch deps + if: steps.cache-deps.outputs.cache-hit != 'true' + run: | + lein deps + + - name: Run tests + run: | + script/test + + - name: Test libraries + run: | + sudo script/install-clojure + script/run_lib_tests + + - name: Build uberjar + run: | + lein with-profiles +reflection do run + lein do clean, uberjar + + - name: Babashka version + id: babashka-version + run: | + BABASHKA_VERSION=$(cat resources/BABASHKA_VERSION) + echo "##[set-output name=version;]${BABASHKA_VERSION}" + + - name: Reflection artifact + run: | + cp reflection.json babashka-${{ steps.babashka-version.outputs.version }}-reflection.json + + - uses: actions/upload-artifact@v1 + with: + name: jar + path: target/babashka-${{ steps.babashka-version.outputs.version }}-standalone.jar + + - uses: actions/upload-artifact@v1 + with: + name: reflection.json + path: babashka-${{ steps.babashka-version.outputs.version }}-reflection.json + + linux: + needs: [jvm] + runs-on: ubuntu-18.04 + steps: + - name: Git checkout + uses: actions/checkout@v1 + with: + fetch-depth: 1 + submodules: 'true' + + - uses: actions/download-artifact@v1 + with: + name: jar + path: . + + - uses: actions/download-artifact@v1 + with: + name: reflection.json + path: . + + - name: Cache deps + uses: actions/cache@v1 + id: cache-deps + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('project.clj') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Cache GraalVM + uses: actions/cache@v1 + id: cache-graalvm + with: + path: ~/graalvm-ce-java8-19.3.1 + key: ${{ runner.os }}-graalvm-19.3.1 + restore-keys: | + ${{ runner.os }}-graalvm-19.3.1 + + - name: Download GraalVM + run: | + cd ~ + if ! [ -d graalvm-ce-java8-19.3.1 ]; then + curl -O -sL https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-19.3.1/graalvm-ce-java8-linux-amd64-19.3.1.tar.gz + tar xzf graalvm-ce-java8-linux-amd64-19.3.1.tar.gz + fi + + - name: Babashka version + id: babashka-version + run: | + BABASHKA_VERSION=$(cat resources/BABASHKA_VERSION) + echo "##[set-output name=version;]${BABASHKA_VERSION}" + + - name: Build Linux native image + run: | + export BABASHKA_JAR=babashka-${{ steps.babashka-version.outputs.version }}-standalone.jar + export BABASHKA_XMX="-J-Xmx6g" + export GRAALVM_HOME="$HOME/graalvm-ce-java8-19.3.1" + cp babashka-${{ steps.babashka-version.outputs.version }}-reflection.json reflection.json + script/compile + + - name: Test binary + run: | + BABASHKA_TEST_ENV=native script/test + + - name: Install clojure + run: | + sudo script/install-clojure /usr/local + + - name: Test libraries + run: | + BABASHKA_TEST_ENV=native script/run_lib_tests + + - uses: actions/upload-artifact@v1 + with: + path: bb + name: babashka-${{ steps.babashka-version.outputs.version }}-linux-amd64.zip + + linux-static: + needs: [jvm] + runs-on: ubuntu-16.04 + steps: + - name: Git checkout + uses: actions/checkout@v1 + with: + fetch-depth: 1 + submodules: 'true' + + - uses: actions/download-artifact@v1 + with: + name: jar + path: . + + - uses: actions/download-artifact@v1 + with: + name: reflection.json + path: . + + - name: Cache deps + uses: actions/cache@v1 + id: cache-deps + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('project.clj') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Cache GraalVM + uses: actions/cache@v1 + id: cache-graalvm + with: + path: ~/graalvm-ce-java8-19.3.1 + key: ${{ runner.os }}-graalvm-19.3.1 + restore-keys: | + ${{ runner.os }}-graalvm-19.3.1 + + - name: Download GraalVM + run: | + cd ~ + if ! [ -d graalvm-ce-java8-19.3.1 ]; then + curl -O -sL https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-19.3.1/graalvm-ce-java8-linux-amd64-19.3.1.tar.gz + tar xzf graalvm-ce-java8-linux-amd64-19.3.1.tar.gz + fi + + - name: Babashka version + id: babashka-version + run: | + BABASHKA_VERSION=$(cat resources/BABASHKA_VERSION) + echo "##[set-output name=version;]${BABASHKA_VERSION}" + + - name: Build Linux native image + run: | + export BABASHKA_JAR=babashka-${{ steps.babashka-version.outputs.version }}-standalone.jar + export BABASHKA_XMX="-J-Xmx6g" + export GRAALVM_HOME="$HOME/graalvm-ce-java8-19.3.1" + export BABASHKA_STATIC=true + cp babashka-${{ steps.babashka-version.outputs.version }}-reflection.json reflection.json + script/compile + + - name: Test binary + run: | + ./bb '(+ 1 2 3)' + BABASHKA_TEST_ENV=native script/test + + - name: Install clojure + run: | + sudo script/install-clojure + + - name: Test libraries + run: | + BABASHKA_TEST_ENV=native script/run_lib_tests + + - uses: actions/upload-artifact@v1 + with: + path: bb + name: babashka-${{ steps.babashka-version.outputs.version }}-linux-static-amd64.zip + + mac: + needs: [jvm] + runs-on: macOS-latest + steps: + - name: Git checkout + uses: actions/checkout@v1 + with: + fetch-depth: 1 + submodules: 'true' + + - uses: actions/download-artifact@v1 + with: + name: jar + path: . + + - uses: actions/download-artifact@v1 + with: + name: reflection.json + path: . + + - name: Cache GraalVM + uses: actions/cache@v1 + id: cache-graalvm + with: + path: ~/graalvm-ce-java8-19.3.1 + key: ${{ runner.os }}-graalvm-19.3.1 + restore-keys: | + ${{ runner.os }}-graalvm-19.3.1 + + - name: Download GraalVM + run: | + cd ~ + if ! [ -d graalvm-ce-java8-19.3.1 ]; then + curl -O -sL https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-19.3.1/graalvm-ce-java8-darwin-amd64-19.3.1.tar.gz + tar xzf graalvm-ce-java8-darwin-amd64-19.3.1.tar.gz + fi + + - name: Babashka version + id: babashka-version + run: | + BABASHKA_VERSION=$(cat resources/BABASHKA_VERSION) + echo "##[set-output name=version;]${BABASHKA_VERSION}" + + - name: Build macOS native image + run: | + export BABASHKA_JAR=babashka-${{ steps.babashka-version.outputs.version }}-standalone.jar + export BABASHKA_XMX="-J-Xmx6g" + export GRAALVM_HOME="$HOME/graalvm-ce-java8-19.3.1/Contents/Home" + cp babashka-${{ steps.babashka-version.outputs.version }}-reflection.json reflection.json + script/compile + + - name: Test binary + run: | + sudo script/install-leiningen + BABASHKA_TEST_ENV=native script/test + + - name: Test libraries + run: | + sudo script/install-clojure + BABASHKA_TEST_ENV=native script/run_lib_tests + + - uses: actions/upload-artifact@v1 + with: + path: bb + name: babashka-${{ steps.babashka-version.outputs.version }}-macos-amd64.zip + + deploy: + needs: [jvm, linux, linux-static, mac] + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + runs-on: ubuntu-18.04 + steps: + - name: Git checkout + uses: actions/checkout@v1 + with: + fetch-depth: 1 + submodules: 'true' + + - name: Cache deps + uses: actions/cache@v1 + id: cache-deps + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('project.clj') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Deploy + env: + CLOJARS_USER: "${{ secrets.CLOJARS_USER }}" + CLOJARS_PASS: "${{ secrets.CLOJARS_PASS }}" + run: | + .github/script/deploy + + docker: + needs: [jvm, linux, linux-static, mac] + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + runs-on: ubuntu-18.04 + steps: + - name: Git checkout + uses: actions/checkout@v1 + with: + fetch-depth: 1 + submodules: 'true' + + - name: Docker build + env: + DOCKERHUB_USER: "${{ secrets.DOCKERHUB_USER }}" + DOCKERHUB_PASS: "${{ secrets.DOCKERHUB_PASS }}" + run: | + .github/script/docker diff --git a/.gitignore b/.gitignore index d8a7040d..1b98c17b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,6 @@ pom.xml.asc !java/src/babashka/impl/LockFix.class !test-resources/babashka/src_for_classpath_test/foo.jar .cpcache -reflection.json +*reflection.json +/tmp +/reports diff --git a/.gitmodules b/.gitmodules index 7c3bc0cf..6bf5f5fe 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,6 @@ path = sci url = https://github.com/borkdude/sci branch = master +[submodule "babashka.curl"] + path = babashka.curl + url = https://github.com/borkdude/babashka.curl diff --git a/CHANGES.md b/CHANGES.md index fff6f671..e3e48050 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,13 +2,18 @@ ## Breaking changes +## v0.0.79 +- [babashka.curl#9](https://github.com/borkdude/babashka.curl/issues/9): + BREAKING! Functions in `babashka.curl` like `get`, `post`, etc. now always + return a map with `:status`, `:body`, and `:headers`. + ## v0.0.71 - #267 Change behavior of reader conditionals: the `:clj` branch is taken when it occurs before a `:bb` branch. ## v0.0.44 - 0.0.45 - #173: Rename `*in*` to `*input*` (in the `user` namespace). The reason for - this is that itt shadowed `clojure.core/*in*` when used unqualified. + this is that it shadowed `clojure.core/*in*` when used unqualified. ## v0.0.43 - #160: Add support for `java.lang.ProcessBuilder`. See docs. This replaces the diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..0a177eb9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1 @@ +See [doc/dev.md](doc/dev.md). diff --git a/Dockerfile b/Dockerfile index 267c3304..97b44d1d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,25 @@ -FROM ubuntu AS BASE +FROM clojure:lein-2.9.1 AS BASE +ARG BABASHKA_XMX="-J-Xmx3g" -RUN apt-get update -RUN apt-get install -yy curl unzip build-essential zlib1g-dev +RUN apt update +RUN apt install --no-install-recommends -yy curl unzip build-essential zlib1g-dev WORKDIR "/opt" RUN curl -sLO https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-19.3.1/graalvm-ce-java8-linux-amd64-19.3.1.tar.gz RUN tar -xzf graalvm-ce-java8-linux-amd64-19.3.1.tar.gz + ENV GRAALVM_HOME="/opt/graalvm-ce-java8-19.3.1" ENV JAVA_HOME="/opt/graalvm-ce-java8-19.3.1/bin" ENV PATH="$PATH:$JAVA_HOME" +ENV BABASHKA_STATIC="true" +ENV BABASHKA_XMX=$BABASHKA_XMX + COPY . . -RUN apt install -y sudo -RUN ./.circleci/script/install-leiningen RUN ./script/compile -RUN cp bb /usr/local/bin -FROM ubuntu:bionic -COPY --from=BASE /usr/local/bin/bb /usr/local/bin +FROM alpine:latest + +RUN apk add --no-cache curl +RUN mkdir -p /usr/local/bin +COPY --from=BASE /opt/bb /usr/local/bin/bb CMD ["bb"] diff --git a/README.md b/README.md index 00c3e269..595e83c9 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ [![CircleCI](https://circleci.com/gh/borkdude/babashka/tree/master.svg?style=shield)](https://circleci.com/gh/borkdude/babashka/tree/master) -[![Clojars Project](https://img.shields.io/clojars/v/borkdude/babashka.svg)](https://clojars.org/borkdude/babashka) [![project chat](https://img.shields.io/badge/slack-join_chat-brightgreen.svg)](https://app.slack.com/client/T03RZGPFR/CLX41ASCS) - - +[![Financial Contributors on Open Collective](https://opencollective.com/babashka/all/badge.svg?label=financial+contributors)](https://opencollective.com/babashka) [![Clojars Project](https://img.shields.io/clojars/v/borkdude/babashka.svg)](https://clojars.org/borkdude/babashka) A Clojure [babushka](https://en.wikipedia.org/wiki/Headscarf) for the grey areas of Bash. @@ -14,80 +12,106 @@ A Clojure [babushka](https://en.wikipedia.org/wiki/Headscarf) for the grey areas @laheadle on Clojurians Slack -## Quickstart +## Introduction -``` shellsession -$ bash <(curl -s https://raw.githubusercontent.com/borkdude/babashka/master/install) -$ ls | bb --time -i '(filter #(-> % io/file .isDirectory) *input*)' -("doc" "resources" "sci" "script" "src" "target" "test") -bb took 4ms. -``` - -## Rationale - -The sweet spot for babashka is executing Clojure snippets or scripts in the same -space where you would use Bash. +The main idea behind babashka is to leverage Clojure in places where you would +be using bash otherwise. As one user described it: > I’m quite at home in Bash most of the time, but there’s a substantial grey area of things that are too complicated to be simple in bash, but too simple to be worth writing a clj/s script for. Babashka really seems to hit the sweet spot for those cases. -Goals: +### Goals -* Fast startup / low latency. This is achieved by compiling to native using [GraalVM](https://github.com/oracle/graal). -* Familiarity and portability. Keep migration barriers between bash and Clojure as low as possible by: - - Gradually introducing Clojure expressions to existing bash scripts - - Scripts written in babashka should also be able to run on the JVM without major changes. -* Multi-threading support similar to Clojure on the JVM -* Batteries included (clojure.tools.cli, core.async, ...) +* Low latency Clojure scripting alternative to JVM Clojure. +* Easy installation: grab the self-contained binary and run. No JVM needed. +* Familiarity and portability: + - Scripts should be compatible with JVM Clojure as much as possible + - Scripts should be platform-independent as much as possible. Babashka offers + support for linux, macOS and Windows. +* Allow interop with commonly used classes like `java.io.File` and `System` +* Multi-threading support (`pmap`, `future`, `core.async`) +* Batteries included (tools.cli, cheshire, ...) +* Library support via popular tools like the `clojure` CLI -Non-goals: +### Non-goals -* Performance -* Provide a mixed Clojure/bash DSL (see portability). +* Performance1 +* Provide a mixed Clojure/Bash DSL (see portability). * Replace existing shells. Babashka is a tool you can use inside existing shells like bash and it is designed to play well with them. It does not aim to replace them. -Babashka uses [sci](https://github.com/borkdude/sci) for interpreting Clojure. Sci -implements a subset of Clojure and is not as performant as compiled code. If your script is taking more than a few seconds, Clojure on the JVM may be a better fit. +1 Babashka uses [sci](https://github.com/borkdude/sci) for +interpreting Clojure. Sci implements a suffiently large subset of +Clojure. Interpreting code is in general not as performant as executing compiled +code. If your script takes more than a few seconds to run, Clojure on the JVM +may be a better fit, since the performance of Clojure on the JVM outweighs its +startup time penalty. Read more about the differences with Clojure +[here](#differences-with-clojure). -Read more about the differences with Clojure [here](#differences-with-clojure). -## Status +### Talk -Experimental. Breaking changes are expected to happen at this phase. Keep an eye -on [CHANGES.md](CHANGES.md) for a list of breaking changes. +To get an overview of babashka, you can watch this talk ([slides](https://speakerdeck.com/borkdude/babashka-and-the-small-clojure-interpreter-at-clojured-2020)): -## Examples +[![Babashka at ClojureD 2020](https://img.youtube.com/vi/Nw8aN-nrdEk/0.jpg)](https://www.youtube.com/watch?v=Nw8aN-nrdEk) + +## Quickstart ``` shellsession -$ ls | bb -i '*input*' -["LICENSE" "README.md" "bb" "doc" "pom.xml" "project.clj" "resources" "script" "src" "target" "test"] +$ curl -s https://raw.githubusercontent.com/borkdude/babashka/master/install -o install-babashka +$ chmod +x install-babashka && ./install-babashka +$ ls | bb --time -i '(filter #(-> % io/file .isDirectory) *input*)' +("doc" "resources" "sci" "script" "src" "target" "test") +bb took 4ms. +``` -$ ls | bb -i '(count *input*)' -12 +### Examples +Read the output from a shell command as a lazy seq of strings: + +``` shell +$ ls | bb -i '(take 2 *input*)' +("CHANGES.md" "Dockerfile") +``` + +Read EDN from stdin and write the result to stdout: + +``` shell $ bb '(vec (dedupe *input*))' <<< '[1 1 1 1 2]' [1 2] +``` -$ bb '(filterv :foo *input*)' <<< '[{:foo 1} {:bar 2}]' -[{:foo 1}] +Read more about input and output flags +[here](https://github.com/borkdude/babashka/#input-and-output-flags). -$ bb '(#(+ %1 %2 %3) 1 2 *input*)' <<< 3 -6 +Execute a script. E.g. print the current time in California using the +`java.time` API: -$ ls | bb -i '(filterv #(re-find #"README" %) *input*)' -["README.md"] +File `pst.clj`: +``` clojure +#!/usr/bin/env bb -$ bb '(run! #(shell/sh "touch" (str "/tmp/test/" %)) (range 100))' -$ ls /tmp/test | bb -i '*input*' -["0" "1" "10" "11" "12" "13" "14" "15" "16" "17" "18" "19" "2" "20" "21" ...] +(def now (java.time.ZonedDateTime/now)) +(def LA-timezone (java.time.ZoneId/of "America/Los_Angeles")) +(def LA-time (.withZoneSameInstant now LA-timezone)) +(def pattern (java.time.format.DateTimeFormatter/ofPattern "HH:mm")) +(println (.format LA-time pattern)) +``` -$ bb -O '(repeat "dude")' | bb --stream '(str *input* "rino")' | bb -I '(take 3 *input*)' -("duderino" "duderino" "duderino") +``` shell +$ pst.clj +05:17 ``` More examples can be found in the [gallery](#gallery). +## Status + +Functionality regarding `clojure.core` and `java.lang` can be considered stable +and is unlikely to change. Changes may happen in other parts of babashka, +although we will try our best to prevent them. Always check the release notes or +[CHANGES.md](CHANGES.md) before upgrading. + ## Installation ### Brew @@ -130,7 +154,13 @@ $ bash <(curl -s https://raw.githubusercontent.com/borkdude/babashka/master/inst ### Download -You may also download a binary from [Github](https://github.com/borkdude/babashka/releases). +You may also download a binary from +[Github](https://github.com/borkdude/babashka/releases). For linux there is a +static binary available which can be used on Alpine. + +## Docker + +Check out the image on [Docker hub](https://hub.docker.com/r/borkdude/babashka/). ## Usage @@ -138,7 +168,7 @@ You may also download a binary from [Github](https://github.com/borkdude/babashk Usage: bb [ -i | -I ] [ -o | -O ] [ --stream ] [--verbose] [ ( --classpath | -cp ) ] [ --uberscript ] [ ( --main | -m ) | -e | -f | - --repl | --socket-repl [:] ] + --repl | --socket-repl [:] | --nrepl-server [:] ] [ arg* ] Options: @@ -160,6 +190,7 @@ Options: -m, --main Call the -main function from namespace with args. --repl Start REPL. Use rlwrap for history. --socket-repl Start socket REPL. Specify port (e.g. 1666) or host and port separated by colon (e.g. 127.0.0.1:1666). + --nrepl-server Start nREPL server. Specify port (e.g. 1667) or host and port separated by colon (e.g. 127.0.0.1:1667). --time Print execution time before exiting. -- Stop parsing args and pass everything after -- to *command-line-args* @@ -184,14 +215,16 @@ enumerated explicitly. `make-parents`, `output-stream`, `reader`, `resource`, `writer` - `clojure.main`: `repl` - [`clojure.core.async`](https://clojure.github.io/core.async/) aliased as - `async`. The `alt` and `go` macros are not available but `alts!!` does work as - it is a function. + `async`. - `clojure.stacktrace` - `clojure.test` - `clojure.pprint`: `pprint` (currently backed by [fipp](https://github.com/brandonbloom/fipp)'s `fipp.edn/pprint`) - [`clojure.tools.cli`](https://github.com/clojure/tools.cli) aliased as `tools.cli` - [`clojure.data.csv`](https://github.com/clojure/data.csv) aliased as `csv` - [`cheshire.core`](https://github.com/dakrone/cheshire) aliased as `json` +- [`cognitect.transit`](https://github.com/cognitect/transit-clj) aliased as `transit` +- [`clj-yaml.core`](https://github.com/clj-commons/clj-yaml) alias as `yaml` +- [`bencode.core`](https://github.com/nrepl/bencode) aliased as `bencode`: `read-bencode`, `write-bencode` A selection of java classes are available, see `babashka/impl/classes.clj`. @@ -311,6 +344,12 @@ $ bb '((fn [x] (println x) (when (not (signal/pipe-signal-received?)) (recur (in The namespace `babashka.signal` is aliased as `signal` in the `user` namespace. +#### babashka.curl + +The namespace `babashka.curl` is a tiny wrapper around curl. It's aliased as +`curl` in the user namespace. See +[babashka.curl](https://github.com/borkdude/babashka.curl). + ## Running a file Scripts may be executed from a file using `-f` or `--file`: @@ -375,6 +414,12 @@ $ cat script.clj ("hello" "1" "2" "3") ``` +## [Running a REPL](doc/repl.md) + +Babashka offers a REPL, a socket REPL and an nREPL server. Look +[here](doc/repl.md) for more information on how to use and integrate them with +your editor. + ## Preloads The environment variable `BABASHKA_PRELOADS` allows to define code that will be @@ -416,6 +461,28 @@ $ bb --classpath src --main my.namespace Hello from my namespace! ``` +So if you have a larger script with a classic Clojure project layout like + +```shellsession +$ tree -L 3 +├── deps.edn +├── README +├── src +│   └── project_namespace +│   ├── main.clj +│   └── utilities.clj +└── test + └── project_namespace + ├── test_main.clj + └── test_utilities.clj +``` +Then you can tell Babashka to include both the `src` and `test` +folders in the classpath and start a socket REPL by running: + +```shellsession +$ bb --classpath src:test --socket-repl 1666 +``` + Note that you can use the `clojure` tool to produce classpaths and download dependencies: ``` shellsession @@ -441,6 +508,13 @@ $ bb "(my-gist-script/-main)" Hello from gist script! ``` +When invoking `bb` with a main function, the expression `(System/getProperty +"babashka.main")` will return the name of the main function. + +Also see the +[babashka.classpath](https://github.com/borkdude/babashka/#babashkaclasspath) +namespace which allows dynamically adding to the classpath. + ### Deps.clj The [`deps.clj`](https://github.com/borkdude/deps.clj/) script can be used to work with `deps.edn`-based projects: @@ -469,6 +543,46 @@ Hello from gist script! nil ``` +You can also use for example `deps.clj` to produce the classpath for a +`babashka` REPL: + +```shellsession +$ cat script/start-repl.sh +#!/bin/sh -e +git_root=$(git rev-parse --show-toplevel) +export BABASHKA_CLASSPATH=$("$git_root"/script/deps.clj -Spath) +bb --socket-repl 1666 +$ ./script/start-repl.sh +Babashka socket REPL started at localhost:1666 +``` + +Now, given that your `deps.edn` and source tree looks something like + +```shellsession +$ cat deps.edn +{:paths ["src" "test"] + :deps {}} +$ tree -L 3 +├── deps.edn +├── README +├── script +│   ├── deps.clj +│   └── start-repl.sh +├── src +│   └── project_namespace +│   ├── main.clj +│   └── utilities.clj +└── test + └── project_namespace + ├── test_main.clj + └── test_utilities.clj + +``` + +you should now be able to `(require '[multi-machine-rsync.utilities :as util])` +in your REPL and the source code in `/src/multi_machine_rsync/utilities.clj` +will be evaluated and made available through the symbol `util`. + ## Uberscript The `--uberscript` option collects the expressions in @@ -547,44 +661,6 @@ bb -cp "src:test:resources" \ (System/exit (+ fail error)))" ``` -## REPL - -Babashka supports both a REPL and socket REPL. To start the REPL, type: - -``` shell -$ bb --repl -``` - -To get history with up and down arrows, use `rlwrap`: - -``` shell -$ rlwrap bb --repl -``` - -To start the socket REPL you can do this: - -``` shellsession -$ bb --socket-repl 1666 -Babashka socket REPL started at localhost:1666 -``` - -Now you can connect with your favorite socket REPL client: - -``` shellsession -$ rlwrap nc 127.0.0.1 1666 -Babashka v0.0.14 REPL. -Use :repl/quit or :repl/exit to quit the REPL. -Clojure rocks, Bash reaches. - -bb=> (+ 1 2 3) -6 -bb=> :repl/quit -$ -``` - -A socket REPL client for Emacs is -[inf-clojure](https://github.com/clojure-emacs/inf-clojure). - ## Spawning and killing a process Use the `java.lang.ProcessBuilder` class. @@ -604,10 +680,9 @@ Also see this [example](examples/process_builder.clj). ## Async -Apart from `future` and `pmap` for creating threads, you may use the `async` -namespace, which maps to `clojure.core.async`, for asynchronous scripting. The -following example shows how to get first available value from two different -processes: +In addition to `future`, `pmap`, `promise` and friends, you may use the +`clojure.core.async` namespace for asynchronous scripting. The following example +shows how to get first available value from two different processes: ``` clojure bb ' @@ -620,30 +695,93 @@ bb ' process 2 ``` +Note: the `go` macro is available for compatibility with JVM programs, but the +implementation maps to `clojure.core.async/thread` and the single exclamation +mark operations (`!`, etc.) map to the double exclamation mark operations +(`!!`, etc.). It will not "park" threads, like on the JVM. + +## HTTP + +For making HTTP requests you can use: + +- [babashka.curl](https://github.com/borkdude/babashka.curl). This library is + included with babashka and aliased as `curl` in the user namespace. +- `slurp` for simple `GET` requests +- [clj-http-lite](https://github.com/borkdude/clj-http-lite) as a library. +- `clojure.java.shell` or `java.lang.ProcessBuilder` for shelling out to your + favorite command line http client + +### HTTP over Unix sockets + +This can be useful for talking to Docker: + +``` clojure +(require '[clojure.java.shell :refer [sh]]) +(require '[cheshire.core :as json]) +(-> (sh "curl" "--silent" + "--no-buffer" "--unix-socket" + "/var/run/docker.sock" + "http://localhost/images/json") + :out + (json/parse-string true) + first + :RepoTags) ;;=> ["borkdude/babashka:latest"] +``` + +## Shutdown hook + +Adding a shutdown hook allows you to execute some code before the script exits. + +``` clojure +$ bb -e '(-> (Runtime/getRuntime) (.addShutdownHook (Thread. #(println "bye"))))' +bye +``` + +This also works when the script is interrupted with ctrl-c. + +## Bencode + +Babashka comes with the [nrepl/bencode](https://github.com/nrepl/bencode) +library which allows you to read and write bencode messages to a socket. A +simple example which evaluates a Clojure expression on an nREPL server started +with `lein repl`: + +``` clojure +(ns nrepl-client + (:require [bencode.core :as b])) + +(defn nrepl-eval [port expr] + (let [s (java.net.Socket. "localhost" port) + out (.getOutputStream s) + in (java.io.PushbackInputStream. (.getInputStream s)) + _ (b/write-bencode out {"op" "eval" "code" expr}) + bytes (get (b/read-bencode in) "value")] + (String. bytes))) + +(nrepl-eval 52054 "(+ 1 2 3)") ;;=> "6" +``` + ## Differences with Clojure Babashka is implemented using the [Small Clojure Interpreter](https://github.com/borkdude/sci). This means that a snippet or script is not compiled to JVM bytecode, but executed form by form by a runtime -which implements a subset of Clojure. Babashka is compiled to a native binary -using [GraalVM](https://github.com/oracle/graal). It comes with a selection of -built-in namespaces and functions from Clojure and other useful libraries. The -data types (numbers, strings, persistent collections) are the +which implements a sufficiently large subset of Clojure. Babashka is compiled to +a native binary using [GraalVM](https://github.com/oracle/graal). It comes with +a selection of built-in namespaces and functions from Clojure and other useful +libraries. The data types (numbers, strings, persistent collections) are the same. Multi-threading is supported (`pmap`, `future`). Differences with Clojure: -- A subset of Java classes are supported. - -- Only the `clojure.core`, `clojure.edn`, `clojue.java.io`, - `clojure.java.shell`, `clojure.set`, `clojure.stacktrace`, `clojure.string`, - `clojure.template`, `clojure.test` and `clojure.walk` namespaces are available - from Clojure. +- A pre-selected set of Java classes are supported. You cannot add Java classes + at runtime. - Interpretation comes with overhead. Therefore tight loops are likely slower - than in Clojure on the JVM. + than in Clojure on the JVM. In general interpretation yields slower programs + than compiled programs. -- No support for unboxed types. +- No `defprotocol`, `defrecord` and unboxed math. ## External resources @@ -682,13 +820,13 @@ Ran 1 tests containing 0 assertions. Requires `bb` >= v0.0.71. Latest coordinates checked with with bb: ``` clojure -{:git/url "https://github.com/weavejester" :sha "a4e5fb5383f5c0d83cb2d005181a35b76d8a136d"} +{:git/url "https://github.com/weavejester/medley" :sha "a4e5fb5383f5c0d83cb2d005181a35b76d8a136d"} ``` Example: ``` shell -$ export BABASHKA_CLASSPATH=$(clojure -Spath -Sdeps '{:deps {medley {:git/url "https://github.com/weavejester" :sha "a4e5fb5383f5c0d83cb2d005181a35b76d8a136d"}}}') +$ export BABASHKA_CLASSPATH=$(clojure -Spath -Sdeps '{:deps {medley {:git/url "https://github.com/weavejester/medley" :sha "a4e5fb5383f5c0d83cb2d005181a35b76d8a136d"}}}') $ bb -e "(require '[medley.core :as m]) (m/index-by :id [{:id 1} {:id 2}])" {1 {:id 1}, 2 {:id 2}} @@ -749,26 +887,81 @@ export BABASHKA_CLASSPATH="$(clojure -Sdeps '{:deps {clojure-csv {:mvn/version " Requires `bb` >= v0.0.71. Latest coordinates checked with with bb: ``` clojure -{:git/url "https://github.com/lambdaisland/regal" :sha "8d300f8e15f43480801766b7762530b6d412c1e6"} +{:git/url "https://github.com/lambdaisland/regal" :sha "d4e25e186f7b9705ebb3df6b21c90714d278efb7"} ``` Example: ``` shell -$ export BABASHKA_CLASSPATH=$(clojure -Spath -Sdeps '{:deps {regal {:git/url "https://github.com/lambdaisland/regal" :sha "8d300f8e15f43480801766b7762530b6d412c1e6"}}}') +$ export BABASHKA_CLASSPATH=$(clojure -Spath -Sdeps '{:deps {regal {:git/url "https://github.com/lambdaisland/regal" :sha "d4e25e186f7b9705ebb3df6b21c90714d278efb7"}}}') $ bb -e "(require '[lambdaisland.regal :as regal]) (regal/regex [:* \"ab\"])" #"(?:\Qab\E)*" ``` -#### [spartan.test](https://github.com/borkdude/spartan.test/) +#### [4bb](https://github.com/porkostomus/4bb) -A minimal test framework compatible with babashka. This library is deprecated -since babashka v0.0.68 which has `clojure.test` built-in. +4clojure as a babashka script! +#### [cprop](https://github.com/tolitius/cprop/) -### Blogs +A clojure configuration libary. Latest test version: `"0.1.16"`. +#### [comb](https://github.com/weavejester/comb) + +Simple templating system for Clojure. Latest tested version: `"0.1.1"`. + +``` clojure +$ export BABASHKA_CLASSPATH=$(clojure -Spath -Sdeps '{:deps {comb {:mvn/version "0.1.1"}}}') +$ rlwrap bb +... +user=> (require '[comb.template :as template]) +user=> (template/eval "<% (dotimes [x 3] %>foo<% ) %>") +"foofoofoo" +user=> (template/eval "Hello <%= name %>" {:name "Alice"}) +"Hello Alice" +user=> (def hello (template/fn [name] "Hello <%= name %>")) +user=> (hello "Alice") +"Hello Alice" +``` + +#### [nubank/docopt](https://github.com/nubank/docopt.clj#babashka) + +Docopt implementation in Clojure, compatible with babashka. + +#### [babashka lambda layer](https://github.com/dainiusjocas/babashka-lambda-layer) + +Babashka Lambda runtime packaged as a Lambda layer. + +#### [Release on push Github action](https://github.com/rymndhng/release-on-push-action) + +Github Action to create a git tag + release when pushed to master. Written in +babashka. + +#### [justone/bb-scripts](https://github.com/justone/bb-scripts) + +A collection of scripts developed by [@justone](https://github.com/justone). + +#### [nativity](https://github.com/MnRA/nativity) + +Turn babashka scripts into binaries using GraalVM `native-image`. + +#### [arrangement](https://github.com/greglook/clj-arrangement) + +A micro-library which provides a total-ordering comparator for Clojure +values. Tested with version `1.2.0`. + +## Package babashka script as a AWS Lambda + +AWS Lambda runtime doesn't support signals, therefore babashka has to disable +handling of the SIGPIPE. This can be done by setting +`BABASHKA_DISABLE_PIPE_SIGNAL_HANDLER` to `true`. + +## Articles, podcasts and videos + +- [Implementing an nREPL server for babashka](https://youtu.be/0YmZYnwyHHc): impromptu presentation by Michiel Borkent at the online [Dutch Clojure Meetup](http://meetup.com/The-Dutch-Clojure-Meetup) +- [ClojureScript podcast](https://soundcloud.com/user-959992602/s3-e5-babashka-with-michiel-borkent) with Jacek Schae interviewing Michiel Borkent +- [Babashka talk at ClojureD](https://www.youtube.com/watch?v=Nw8aN-nrdEk) ([slides](https://speakerdeck.com/borkdude/babashka-and-the-small-clojure-interpreter-at-clojured-2020)) by Michiel Borkent - [Babashka: a quick example](https://juxt.pro/blog/posts/babashka.html) by Malcolm Sparks - [Clojure Start Time in 2019](https://stuartsierra.com/2019/12/21/clojure-start-time-in-2019) by Stuart Sierra - [Advent of Random @@ -837,13 +1030,18 @@ $ < /tmp/test.txt bb -io '(shuffle *input*)' ### Fetch latest Github release tag -For converting JSON to EDN, see [jet](https://github.com/borkdude/jet). +``` shell +(require '[clojure.java.shell :refer [sh]] + '[cheshire.core :as json]) -``` shellsession -$ curl -s https://api.github.com/repos/borkdude/babashka/tags | -jet --from json --keywordize --to edn | -bb '(-> *input* first :name (subs 1))' -"0.0.4" +(defn babashka-latest-version [] + (-> (sh "curl" "https://api.github.com/repos/borkdude/babashka/tags") + :out + (json/parse-string true) + first + :name)) + +(babashka-latest-version) ;;=> "v0.0.73" ``` ### Generate deps.edn entry for a gitlib @@ -938,6 +1136,8 @@ bb '(let [{:keys [dependencies source-paths resource-paths]} (apply hash-map (dr jet --pretty > deps.edn ``` +A script with the same goal can be found [here](https://gist.github.com/swlkr/3f346c66410e5c60c59530c4413a248e#gistcomment-3232605). + ### Print current time in California See [examples/pst.clj](https://github.com/borkdude/babashka/blob/master/examples/pst.clj) @@ -960,13 +1160,132 @@ clojure.core/ffirst Same as (first (first x)) ``` +### Cryptographic hash + +`sha1.clj`: +``` clojure +#!/usr/bin/env bb + +(defn sha1 + [s] + (let [hashed (.digest (java.security.MessageDigest/getInstance "SHA-1") + (.getBytes s)) + sw (java.io.StringWriter.)] + (binding [*out* sw] + (doseq [byte hashed] + (print (format "%02X" byte)))) + (str sw))) + +(sha1 (first *command-line-args*)) +``` + +``` shell +$ sha1.clj babashka +"0AB318BE3A646EEB1E592781CBFE4AE59701EDDF" +``` + +### Package script as Docker image + +`Dockerfile`: +``` dockerfile +FROM borkdude/babashka +RUN echo $'\ +(println "Your command line args:" *command-line-args*)\ +'\ +>> script.clj + +ENTRYPOINT ["bb", "script.clj"] +``` + +``` shell +$ docker build . -t script +... +$ docker run --rm script 1 2 3 +Your command line args: (1 2 3) +``` + +### Extract single file from zip + +``` clojure +;; Given the following: + +;; $ echo 'contents' > file +;; $ zip zipfile.zip file +;; $ rm file + +;; we extract the single file from the zip archive using java.nio: + +(import '[java.nio.file Files FileSystems CopyOption]) +(let [zip-file (io/file "zipfile.zip") + file (io/file "file") + fs (FileSystems/newFileSystem (.toPath zip-file) nil) + file-in-zip (.getPath fs "file" (into-array String []))] + (Files/copy file-in-zip (.toPath file) + (into-array CopyOption []))) +``` + +### Note taking app + +See +[examples/notes.clj](https://github.com/borkdude/babashka/blob/master/examples/notes.clj). This +is a variation on the +[http-server](https://github.com/borkdude/babashka/#tiny-http-server) +example. If you get prompted with a login, use `admin`/`admin`. + + + +### which + +The `which` command re-implemented in Clojure. See +[examples/which.clj](https://github.com/borkdude/babashka/blob/master/examples/which.clj). +Prints the canonical file name. + +``` shell +$ examples/which.clj rg +/usr/local/Cellar/ripgrep/11.0.1/bin/rg +``` + ## Thanks - [adgoji](https://www.adgoji.com/) for financial support +- [CircleCI](https://circleci.com/) for CI and additional support +- [Nikita Prokopov](https://github.com/tonsky) for the logo +- [contributors](https://github.com/borkdude/babashka/graphs/contributors) and + other users posting issues with bug reports and ideas + +## Contributors + +### Code Contributors + +This project exists thanks to all the people who contribute. [[Contribute](doc/dev.md)]. + + +### Financial Contributors + +Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/babashka/contribute)] + +#### Individuals + + + +#### Organizations + +Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/babashka/contribute)] + + + + + + + + + + + ## License -Copyright © 2019 Michiel Borkent +Copyright © 2019-2020 Michiel Borkent Distributed under the EPL License. See LICENSE. diff --git a/assets/notes-example.png b/assets/notes-example.png new file mode 100644 index 00000000..4434ef35 Binary files /dev/null and b/assets/notes-example.png differ diff --git a/babashka.curl b/babashka.curl new file mode 160000 index 00000000..a9e9fe83 --- /dev/null +++ b/babashka.curl @@ -0,0 +1 @@ +Subproject commit a9e9fe83d56b020071c1a3bbeb4656e53c8a988d diff --git a/deps.edn b/deps.edn index 2c28c015..1d43466f 100644 --- a/deps.edn +++ b/deps.edn @@ -1,7 +1,7 @@ -{:paths ["src" "sci/src" "resources" "sci/resources"], - :deps {org.clojure/clojure {:mvn/version "1.10.1"}, +{:paths ["src" "sci/src" "babashka.curl/src" "resources" "sci/resources"], + :deps {org.clojure/clojure {:mvn/version "1.10.2-alpha1"}, org.clojure/tools.reader {:mvn/version "1.3.2"}, - borkdude/edamame {:mvn/version "0.0.10"}, + borkdude/edamame {:mvn/version "0.0.11-alpha.9"}, borkdude/graal.locking {:mvn/version "0.0.2"}, borkdude/sci.impl.reflector {:mvn/version "0.0.1"} org.clojure/core.async {:mvn/version "1.0.567"}, @@ -9,7 +9,9 @@ org.clojure/data.csv {:mvn/version "1.0.0"}, cheshire {:mvn/version "5.10.0"} org.clojure/data.xml {:mvn/version "0.2.0-alpha6"} - fipp {:mvn/version "0.6.22"}} + fipp {:mvn/version "0.6.22"} + clj-commons/clj-yaml {:mvn/version "0.7.1"} + com.cognitect/transit-clj {:mvn/version "1.0.324"}} :aliases {:main {:main-opts ["-m" "babashka.main"]} :profile diff --git a/doc/dev.md b/doc/dev.md index a5c1b8da..2ffae17f 100644 --- a/doc/dev.md +++ b/doc/dev.md @@ -1,5 +1,9 @@ # Developing Babashka +You need [lein](https://leiningen.org/) for running JVM tests and/or producing uberjars. For building binaries you need GraalVM. Currently we use java8-19.3.1. + +## Clone repository + To work on Babashka itself make sure Git submodules are checked out. ``` shellsession @@ -12,8 +16,6 @@ To update later on: $ git submodule update --recursive ``` -You need [Leiningen](https://leiningen.org/), and for building binaries you need GraalVM. - ## REPL `lein repl` will get you a standard REPL/nREPL connection. To work on tests use `lein with-profiles +test repl`. @@ -47,16 +49,40 @@ To build this project, set `$GRAALVM_HOME` to the GraalVM distribution directory Then run: - script/compile + $ script/compile + +To tweak maximum heap size: + +``` +$ BABASHKA_XMX="-J-Xmx4g" script/compile +``` ## Binary size Keep notes here about how adding libraries and classes to Babashka affects the binary size. +We're registering the size of the macOS binary (as built on CircleCI). -We're only registering the size of the macOS binary (as built on CircleCI). +2020/03/29 Added clj-yaml for parsing and generating yaml. +45196996 - 42626884 = 2570kb added. -2020/01/08, ..., 38.7mb / 11.3mb zipped -Added: `clojure.data.xml`. Growth: 1.8mb / 0.4mb zipped. +2020/03/28 Added java.nio.file.FileSystem(s) to support extracting zip files +42562284 - 42021244 = 541kb added. + +2020/03/22 Added java.io.FileReader +42025276 - 42008876 = 16kb added. + +2020/03/20 Added transit write, writer, read, reader +42004796 - 41025212 = 980kb added (305kb zipped). + +2020/03/19 Added java.lang.NumberFormatException, java.lang.RuntimeException, +java.util.MissingResourceException and java.util.Properties to support +[cprop](https://github.com/tolitius/cprop/). +41025180 - 40729908 = 295kb added. + +2020/02/21 +Added java.time.temporal.ChronoUnit +40651596 - 40598260 = 53kb added. +>>>>>>> master 2020/02/19, e43727955a2cdabd2bb0189c20dd7f9a18156fc9 Added fipp.edn/pprint diff --git a/doc/repl.md b/doc/repl.md new file mode 100644 index 00000000..2cdb836e --- /dev/null +++ b/doc/repl.md @@ -0,0 +1,128 @@ +# Running a REPL + +Babashka supports running a REPL, a socket REPL and an nREPL server. + +## REPL + +To start the REPL, type: + +``` shell +$ bb --repl +``` + +To get history with up and down arrows, use `rlwrap`: + +``` shell +$ rlwrap bb --repl +``` + +## Socket REPL + +To start the socket REPL you can do this: + +``` shell +$ bb --socket-repl 1666 +Babashka socket REPL started at localhost:1666 +``` + +Now you can connect with your favorite socket REPL client: + +``` shell +$ rlwrap nc 127.0.0.1 1666 +Babashka v0.0.14 REPL. +Use :repl/quit or :repl/exit to quit the REPL. +Clojure rocks, Bash reaches. + +bb=> (+ 1 2 3) +6 +bb=> :repl/quit +$ +``` + +Editor plugins and tools known to work with a babashka socket REPL: + +- Emacs: [inf-clojure](https://github.com/clojure-emacs/inf-clojure): + + To connect: + + `M-x inf-clojure-connect localhost 1666` + + Before evaluating from a Clojure buffer: + + `M-x inf-clojure-minor-mode` + +- Atom: [Chlorine](https://github.com/mauricioszabo/atom-chlorine) +- Vim: [vim-iced](https://github.com/liquidz/vim-iced) +- IntelliJ IDEA: [Cursive](https://cursive-ide.com/) + + Note: you will have to use a workaround via + [tubular](https://github.com/mfikes/tubular). For more info, look + [here](https://cursive-ide.com/userguide/repl.html#repl-types). + + +## nREPL + +To start an nREPL server: + +``` shell +$ bb --nrepl-server 1667 +``` + +Then connect with your favorite nREPL client: + +``` clojure +$ lein repl :connect 1667 +Connecting to nREPL at 127.0.0.1:1667 +user=> (+ 1 2 3) +6 +user=> +``` + +Editor plugins and tools known to work with the babashka nREPL server: + + - Emacs: [CIDER](https://docs.cider.mx/cider-nrepl/) + - `lein repl :connect` + - VSCode: [Calva](http://calva.io/) + - Atom: [Chlorine](https://github.com/mauricioszabo/atom-chlorine) + - (Neo)Vim: [vim-iced](https://github.com/liquidz/vim-iced), [conjure](https://github.com/Olical/conjure), [fireplace](https://github.com/tpope/vim-fireplace) + +The babashka nREPL server does not write an `.nrepl-port` file at startup, but +you can easily write a script that launches the server and writes the file: + +``` clojure +#!/usr/bin/env bb + +(import [java.net ServerSocket] + [java.io File] + [java.lang ProcessBuilder$Redirect]) + +(require '[babashka.wait :as wait]) + +(let [nrepl-port (with-open [sock (ServerSocket. 0)] (.getLocalPort sock)) + pb (doto (ProcessBuilder. (into ["bb" "--nrepl-server" (str nrepl-port)] + *command-line-args*)) + (.redirectOutput ProcessBuilder$Redirect/INHERIT)) + proc (.start pb)] + (wait/wait-for-port "localhost" nrepl-port) + (spit ".nrepl-port" nrepl-port) + (.deleteOnExit (File. ".nrepl-port")) + (.waitFor proc)) +``` + +### Debugging the nREPL server + +To debug the nREPL server from the binary you can run: + +``` shell +$ BABASHKA_DEV=true bb --nrepl-server 1667 +``` + +This will print all the incoming messages. + +To debug the nREPL server from source: + +``` clojure +$ git clone https://github.com/borkdude/babashka --recursive +$ cd babashka +$ BABASHKA_DEV=true clojure -A:main --nrepl-server 1667 +``` diff --git a/examples/notes.clj b/examples/notes.clj new file mode 100755 index 00000000..c23769ef --- /dev/null +++ b/examples/notes.clj @@ -0,0 +1,147 @@ +#!/usr/bin/env bb + +(import (java.net ServerSocket)) +(require '[clojure.java.io :as io] + '[clojure.string :as str]) + +(def debug? true) +(def user "admin") +(def password "admin") +(def base64 (-> (.getEncoder java.util.Base64) + (.encodeToString (.getBytes (str user ":" password))))) + +(def notes-file (io/file (System/getProperty "user.home") ".notes" "notes.txt")) +(def file-lock (Object.)) + +(defn write-note! [note] + (locking file-lock + (io/make-parents notes-file) + (spit notes-file (str note "\n") :append true))) + +;; hiccup-like +(defn html [v] + (cond (vector? v) + (let [tag (first v) + attrs (second v) + attrs (when (map? attrs) attrs) + elts (if attrs (nnext v) (next v)) + tag-name (name tag)] + (format "<%s%s>%s\n" tag-name (html attrs) (html elts) tag-name)) + (map? v) + (str/join "" + (map (fn [[k v]] + (format " %s=\"%s\"" (name k) v)) v)) + (seq? v) + (str/join " " (map html v)) + :else (str v))) + +(defn write-response [out session-id status headers content] + (let [cookie-header (str "Set-Cookie: notes-id=" session-id) + headers (str/join "\r\n" (conj headers cookie-header)) + response (str "HTTP/1.1 " status "\r\n" + (str headers "\r\n") + "Content-Length: " (if content (count content) + 0) + "\r\n\r\n" + (when content + (str content)))] + (when debug? (println response)) + (binding [*out* out] + (print response) + (flush)))) + +;; the home page +(defn home-response [out session-id] + (let [body (str + "\n" + (html + [:html + [:head + [:title "Notes"]] + [:body + [:h1 "Notes"] + [:pre (when (.exists notes-file) + (slurp notes-file))] + [:form {:action "/" :method "post"} + [:input {:type "text" :name "note"}] + [:input {:type "submit" :value "Submit"}]]]]))] + (write-response out session-id "200 OK" nil body))) + +(defn basic-auth-response [out session-id] + (write-response out session-id + "401 Unauthorized" + ["WWW-Authenticate: Basic realm=\"notes\""] + nil)) + +(def known-sessions + (atom #{})) + +(defn new-session! [] + (let [uuid (str (java.util.UUID/randomUUID))] + (swap! known-sessions conj uuid) + uuid)) + +(defn get-session-id [headers] + (if-let [cookie-header (first (filter #(str/starts-with? % "Cookie: ") headers))] + (let [parts (str/split cookie-header #"; ")] + (if-let [notes-id (first (filter #(str/starts-with? % "notes-id") parts))] + (str/replace notes-id "notes-id=" "") + (new-session!))) + (new-session!))) + +(defn basic-auth-header [headers] + (some #(str/starts-with? % "Basic-Auth: ") headers)) + +(def authenticated-sessions + (atom #{})) + +(defn authenticate! [session-id headers] + (or (contains? @authenticated-sessions session-id) + (when (some #(= % (str "Authorization: Basic " base64)) headers) + (swap! authenticated-sessions conj session-id) + true))) + +;; run the server +(with-open [server-socket (let [s (new ServerSocket 8080)] + (println "Server started on port 8080.") + s)] + (loop [] + (let [client-socket (.accept server-socket)] + (future + (with-open [conn client-socket] + (try + (let [out (io/writer (.getOutputStream conn)) + is (.getInputStream conn) + in (io/reader is) + [_req & headers :as response] + (loop [headers []] + (let [line (.readLine in)] + (if (str/blank? line) + headers + (recur (conj headers line))))) + session-id (get-session-id headers) + form-data (let [sb (StringBuilder.)] + (loop [] + (when (.ready in) + (.append sb (char (.read in))) + (recur))) + (-> (str sb) + (java.net.URLDecoder/decode))) + _ (when debug? (println (str/join "\n" response))) + _ (when-not (str/blank? form-data) + (when debug? (println form-data)) + (let [note (str/replace form-data "note=" "")] + (write-note! note))) + _ (when debug? (println))] + (cond + ;; if we didn't see this session before, we want the user to re-authenticate + (not (contains? @known-sessions session-id)) + (let [uuid (new-session!)] + (basic-auth-response out uuid)) + (not (authenticate! session-id headers)) + (basic-auth-response out session-id) + :else (home-response out session-id))) + (catch Throwable t + (binding [*err* *out*] + (println t))))))) + (recur))) diff --git a/examples/which.clj b/examples/which.clj new file mode 100755 index 00000000..8bc43cee --- /dev/null +++ b/examples/which.clj @@ -0,0 +1,17 @@ +#!/usr/bin/env bb + +(require '[clojure.java.io :as io]) + +(defn which [executable] + (let [path (System/getenv "PATH") + paths (.split path (System/getProperty "path.separator"))] + (loop [paths paths] + (when-first [p paths] + (let [f (io/file p executable)] + (if (and (.isFile f) + (.canExecute f)) + (.getCanonicalPath f) + (recur (rest paths)))))))) + +(when-let [executable (first *command-line-args*)] + (println (which executable))) diff --git a/project.clj b/project.clj index 8bd811f8..37f95090 100644 --- a/project.clj +++ b/project.clj @@ -7,11 +7,13 @@ :url "https://github.com/borkdude/babashka"} :license {:name "Eclipse Public License 1.0" :url "http://opensource.org/licenses/eclipse-1.0.php"} - :source-paths ["src" "sci/src"] + :source-paths ["src" "sci/src" "babashka.curl/src"] + ;; for debugging Reflector.java code: + ;; :java-source-paths ["sci/reflector/src-java"] :resource-paths ["resources" "sci/resources"] - :dependencies [[org.clojure/clojure "1.10.1"] + :dependencies [[org.clojure/clojure "1.10.2-alpha1"] [org.clojure/tools.reader "1.3.2"] - [borkdude/edamame "0.0.10"] + [borkdude/edamame "0.0.11-alpha.9"] [borkdude/graal.locking "0.0.2"] [borkdude/sci.impl.reflector "0.0.1"] [org.clojure/core.async "1.0.567"] @@ -19,7 +21,9 @@ [org.clojure/data.csv "1.0.0"] [org.clojure/data.xml "0.2.0-alpha6"] [cheshire "5.10.0"] - [fipp "0.6.22"]] + [fipp "0.6.22"] + [clj-commons/clj-yaml "0.7.1"] + [com.cognitect/transit-clj "1.0.324"]] :profiles {:test {:dependencies [[clj-commons/conch "0.9.2"] [com.clojure-goes-fast/clj-async-profiler "0.4.0"]]} :uberjar {:global-vars {*assert* false} diff --git a/resources/BABASHKA_RELEASED_VERSION b/resources/BABASHKA_RELEASED_VERSION index cf9f1739..4c4317b7 100644 --- a/resources/BABASHKA_RELEASED_VERSION +++ b/resources/BABASHKA_RELEASED_VERSION @@ -1 +1 @@ -0.0.71 \ No newline at end of file +0.0.86 \ No newline at end of file diff --git a/resources/BABASHKA_VERSION b/resources/BABASHKA_VERSION index 75b9c2a8..baa5f3ce 100644 --- a/resources/BABASHKA_VERSION +++ b/resources/BABASHKA_VERSION @@ -1 +1 @@ -0.0.72-SNAPSHOT \ No newline at end of file +0.0.87-SNAPSHOT \ No newline at end of file diff --git a/resources/CutOffCoreServicesDependencies.java b/resources/CutOffCoreServicesDependencies.java new file mode 100644 index 00000000..50f316b1 --- /dev/null +++ b/resources/CutOffCoreServicesDependencies.java @@ -0,0 +1,19 @@ +import org.graalvm.nativeimage.Platform; +import org.graalvm.nativeimage.Platforms; +import com.oracle.svm.core.annotate.TargetClass; +import com.oracle.svm.core.annotate.Delete; + +public final class CutOffCoreServicesDependencies { +} + +// @Platforms(Platform.DARWIN.class) +// @TargetClass(className = "sun.net.spi.DefaultProxySelector") +// @Delete +// final class Target_sun_net_spi_DefaultProxySelector { +// } + +@Platforms(Platform.DARWIN.class) +@TargetClass(className = "apple.security.KeychainStore") +@Delete +final class Target_apple_security_KeychainStore { +} diff --git a/sci b/sci index eebb4566..ead5dd7c 160000 --- a/sci +++ b/sci @@ -1 +1 @@ -Subproject commit eebb456628beb2ac0d1e31c2be46ee0683b9ee7a +Subproject commit ead5dd7c25e0e38cb6244077ec9e57e00665cde9 diff --git a/script/compile b/script/compile index fbb59dc7..69ed5424 100755 --- a/script/compile +++ b/script/compile @@ -2,48 +2,58 @@ set -eo pipefail +if [ -z "$BABASHKA_XMX" ]; then + export BABASHKA_XMX="-J-Xmx3g" +fi + if [ -z "$GRAALVM_HOME" ]; then echo "Please set GRAALVM_HOME" exit 1 fi -if [ -z "$BABASHKA_XMX" ]; then - export BABASHKA_XMX="-J-Xmx3g" -fi - -"$GRAALVM_HOME/bin/gu" install native-image || true +$GRAALVM_HOME/bin/gu install native-image BABASHKA_VERSION=$(cat resources/BABASHKA_VERSION) -# # We also need to AOT sci, else something didn't work in the Mac build on CircleCI -# # See https://github.com/oracle/graal/issues/1613 -# ( cd /tmp; git clone https://github.com/borkdude/sci 2> /dev/null || true ) -# mkdir -p src/sci -# cp -R /tmp/sci/src/* src - export JAVA_HOME=$GRAALVM_HOME -lein with-profiles +reflection do run -lein do clean, uberjar +SVM_JAR=$(find "$GRAALVM_HOME" | grep svm.jar) +$GRAALVM_HOME/bin/javac -cp "$SVM_JAR" resources/CutOffCoreServicesDependencies.java -$GRAALVM_HOME/bin/native-image \ - -jar target/babashka-$BABASHKA_VERSION-standalone.jar \ - -H:Name=bb \ - -H:+ReportExceptionStackTraces \ - -J-Dclojure.spec.skip-macros=true \ - -J-Dclojure.compiler.direct-linking=true \ - "-H:IncludeResources=BABASHKA_VERSION" \ - "-H:IncludeResources=SCI_VERSION" \ - -H:ReflectionConfigurationFiles=reflection.json \ - --initialize-at-run-time=java.lang.Math\$RandomNumberGeneratorHolder \ - --initialize-at-build-time \ - -H:Log=registerResource: \ - -H:EnableURLProtocols=http,https \ - --enable-all-security-services \ - -H:+JNI \ - --verbose \ - --no-fallback \ - --no-server \ - "$BABASHKA_XMX" +if [ -z "$BABASHKA_JAR" ]; then + lein with-profiles +reflection do run + lein do clean, uberjar + BABASHKA_JAR=${BABASHKA_JAR:-"target/babashka-$BABASHKA_VERSION-standalone.jar"} +fi -lein clean +BABASHKA_BINARY=${BABASHKA_BINARY:-"bb"} + +args=( -jar $BABASHKA_JAR \ + -H:Name=$BABASHKA_BINARY \ + -H:+ReportExceptionStackTraces \ + -J-Dclojure.spec.skip-macros=true \ + -J-Dclojure.compiler.direct-linking=true \ + "-H:IncludeResources=BABASHKA_VERSION" \ + "-H:IncludeResources=SCI_VERSION" \ + -H:ReflectionConfigurationFiles=reflection.json \ + --initialize-at-run-time=java.lang.Math\$RandomNumberGeneratorHolder \ + --initialize-at-build-time \ + -H:Log=registerResource: \ + -H:EnableURLProtocols=http,https \ + --enable-all-security-services \ + -H:+JNI \ + --verbose \ + --no-fallback \ + --no-server \ + --report-unsupported-elements-at-runtime \ + "$BABASHKA_XMX" ) + +if [ "$BABASHKA_STATIC" = "true" ]; then + args+=("--static") +fi + +$GRAALVM_HOME/bin/native-image "${args[@]}" + +if [ ! -z "$(command -v lein)" ]; then + lein clean +fi diff --git a/.circleci/script/install-clojure b/script/install-clojure similarity index 95% rename from .circleci/script/install-clojure rename to script/install-clojure index b4cb4a55..f4781ba1 100755 --- a/.circleci/script/install-clojure +++ b/script/install-clojure @@ -1,6 +1,6 @@ #!/usr/bin/env bash -install_dir=${1:-/tmp/clojure} +install_dir=${1:-/usr/local} mkdir -p "$install_dir" cd /tmp curl -O -sL https://download.clojure.org/install/clojure-tools-1.10.1.447.tar.gz diff --git a/script/install-leiningen b/script/install-leiningen new file mode 100755 index 00000000..92a62a41 --- /dev/null +++ b/script/install-leiningen @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +curl https://raw.githubusercontent.com/technomancy/leiningen/2.9.1/bin/lein > lein +mkdir -p /usr/local/bin/ +mv lein /usr/local/bin/lein +chmod a+x /usr/local/bin/lein +lein self-install diff --git a/script/lib_tests/arrangement_test b/script/lib_tests/arrangement_test new file mode 100755 index 00000000..fa93438f --- /dev/null +++ b/script/lib_tests/arrangement_test @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -eo pipefail + +export BABASHKA_CLASSPATH=$(clojure -Sdeps '{:deps {mvxcvi/arrangement {:mvn/version "1.2.0"}}}' -Spath) + +if [ "$BABASHKA_TEST_ENV" = "native" ]; then + BB_CMD="./bb" +else + BB_CMD="lein bb" +fi + +$BB_CMD -e "(require '[arrangement.core :as order]) (sort order/rank ['a false 2 :b nil 3.14159 \"c\" true \d [3 2] #{:one :two} [3 1 2] #{:three}])" diff --git a/script/lib_tests/babashka_curl_test b/script/lib_tests/babashka_curl_test new file mode 100755 index 00000000..84f60151 --- /dev/null +++ b/script/lib_tests/babashka_curl_test @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +set -eo pipefail + +export BABASHKA_CLASSPATH=$(clojure -Sdeps '{:deps {babasha.curl {:local/root "babashka.curl"}}}' -Spath) + +if [ "$BABASHKA_TEST_ENV" = "native" ]; then + BB_CMD="./bb" +else + BB_CMD="lein bb" +fi + +$BB_CMD -e " +(require '[babashka.curl :as curl] :reload-all) + +(prn (:status (curl/get \"https://www.clojure.org\"))) + +(prn (:status (curl/get \"https://postman-echo.com/get?foo1=bar1&foo2=bar2\"))) + +(prn (:status (curl/post \"https://postman-echo.com/post\"))) + +(prn (:status (curl/post \"https://postman-echo.com/post\" + {:body (json/generate-string {:a 1}) + :headers {\"X-Hasura-Role\" \"admin\"} + :content-type :json + :accept :json}))) + +(prn (:status (curl/put \"https://postman-echo.com/put\" + {:body (json/generate-string {:a 1}) + :headers {\"X-Hasura-Role\" \"admin\"} + :content-type :json + :accept :json}))) +" diff --git a/script/lib_tests/clj_yaml_test b/script/lib_tests/clj_yaml_test new file mode 100755 index 00000000..a20a5279 --- /dev/null +++ b/script/lib_tests/clj_yaml_test @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -eo pipefail + +if [ "$BABASHKA_TEST_ENV" = "native" ]; then + BB_CMD="./bb" +else + BB_CMD="lein bb" +fi + +$BB_CMD -cp test-resources/lib_tests -e " +(require '[clojure.java.io :as io]) +(require '[clj-yaml.core-test]) +(require '[clojure.test :as t]) +(let [{:keys [:test :pass :fail :error]} (t/run-tests 'clj-yaml.core-test)] + (when-not (pos? test) + (System/exit 1)) + (System/exit (+ fail error))) +" diff --git a/script/lib_tests/clojure_data_csv_test b/script/lib_tests/clojure_data_csv_test new file mode 100755 index 00000000..c229ab92 --- /dev/null +++ b/script/lib_tests/clojure_data_csv_test @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -eo pipefail + +if [ "$BABASHKA_TEST_ENV" = "native" ]; then + BB_CMD="./bb" +else + BB_CMD="lein bb" +fi + +$BB_CMD -cp test-resources/lib_tests -e " +(require '[clojure.java.io :as io]) +(require '[clojure.data.csv-test]) +(require '[clojure.test :as t]) +(let [{:keys [:test :pass :fail :error]} (t/run-tests 'clojure.data.csv-test)] + (when-not (pos? test) + (System/exit 1)) + (System/exit (+ fail error))) +" diff --git a/script/lib_tests/comb_test b/script/lib_tests/comb_test new file mode 100755 index 00000000..3e6e88df --- /dev/null +++ b/script/lib_tests/comb_test @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -eo pipefail + +export BABASHKA_CLASSPATH=$(clojure -Spath -Sdeps '{:deps {comb {:mvn/version "0.1.1"}}}') + +if [ "$BABASHKA_TEST_ENV" = "native" ]; then + BB_CMD="./bb" +else + BB_CMD="lein bb" +fi + + +$BB_CMD ' +(ns foo (:require [comb.template :as template])) +(prn (template/eval "<% (dotimes [x 3] %>foo<% ) %>")) +(prn (template/eval "Hello <%= name %>" {:name "Alice"})) +(def hello + (template/fn [name] "Hello <%= name %>")) +(prn (hello "Alice")) +' diff --git a/script/lib_tests/cprop_test b/script/lib_tests/cprop_test new file mode 100755 index 00000000..71ef87bd --- /dev/null +++ b/script/lib_tests/cprop_test @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -eo pipefail + +export BABASHKA_CLASSPATH=$(clojure -Spath -Sdeps '{:deps {cprop {:mvn/version "0.1.16"}}}') + +if [ "$BABASHKA_TEST_ENV" = "native" ]; then + BB_CMD="./bb" +else + BB_CMD="lein bb" +fi + + +CPROP_ENV="hello" $BB_CMD " +(require '[cprop.core :refer [load-config]]) +(require '[cprop.source :refer [from-system-props from-env]]) +(println (:cprop-env (from-env))) +" diff --git a/script/lib_tests/regal_test b/script/lib_tests/regal_test index 76dcca5a..4073c9e6 100755 --- a/script/lib_tests/regal_test +++ b/script/lib_tests/regal_test @@ -2,7 +2,7 @@ set -eo pipefail -export BABASHKA_CLASSPATH="$(clojure -Sdeps '{:deps {regal {:git/url "https://github.com/lambdaisland/regal" :sha "8d300f8e15f43480801766b7762530b6d412c1e6"}}}' -Spath)" +export BABASHKA_CLASSPATH="$(clojure -Sdeps '{:deps {regal {:git/url "https://github.com/lambdaisland/regal" :sha "b059fdb06d5586a9a04c27e7b011c467ad8546db"}}}' -Spath)" if [ "$BABASHKA_TEST_ENV" = "native" ]; then BB_CMD="./bb" @@ -10,4 +10,13 @@ else BB_CMD="lein bb" fi -$BB_CMD "(require '[lambdaisland.regal :as re]) (re/regex [:range \a \z])" +$BB_CMD " +(require '[lambdaisland.regal :as regal]) +(def r [:cat + [:+ [:class [\a \z]]] + \"=\" + [:+ [:not \=]]]) + +(prn (regal/regex r)) +(prn (re-matches (regal/regex r) \"foo=bar\")) +" diff --git a/script/reflection.clj b/script/reflection.clj new file mode 100755 index 00000000..315872c0 --- /dev/null +++ b/script/reflection.clj @@ -0,0 +1,9 @@ +#!/usr/bin/env bb + +(require '[clojure.java.io :as io] + '[clojure.java.shell :refer [sh]] + '[clojure.string :as str]) + +(def version (str/trim (slurp (io/file "resources" "BABASHKA_VERSION")))) +(sh "lein" "with-profiles" "+reflection" "run") +(io/copy (io/file "reflection.json") (io/file (str "babashka-" version "-reflection.json"))) diff --git a/script/run_lib_tests b/script/run_lib_tests index 8e0695a9..b50b845d 100755 --- a/script/run_lib_tests +++ b/script/run_lib_tests @@ -8,3 +8,9 @@ script/lib_tests/spartan_spec_test script/lib_tests/clojure_csv_test script/lib_tests/regal_test script/lib_tests/medley_test +script/lib_tests/babashka_curl_test +script/lib_tests/cprop_test +script/lib_tests/comb_test +script/lib_tests/arrangement_test +script/lib_tests/clj_yaml_test +script/lib_tests/clojure_data_csv_test diff --git a/script/test b/script/test index 58f31d5f..d6c8473b 100755 --- a/script/test +++ b/script/test @@ -3,13 +3,16 @@ set -eo pipefail BABASHKA_PRELOADS="" BABASHKA_CLASSPATH="" +echo "running tests part 1" lein test "$@" BABASHKA_PRELOADS='(defn __bb__foo [] "foo") (defn __bb__bar [] "bar")' BABASHKA_PRELOADS_TEST=true +echo "running tests part 2" lein test :only babashka.main-test/preloads-test BABASHKA_PRELOADS="(require '[env-ns])" BABASHKA_CLASSPATH_TEST=true BABASHKA_CLASSPATH="test-resources/babashka/src_for_classpath_test/env" +echo "running tests part 3" lein test :only babashka.classpath-test/classpath-env-test diff --git a/src/babashka/impl/async.clj b/src/babashka/impl/async.clj index 9cfa421a..6d2cf7a0 100644 --- a/src/babashka/impl/async.clj +++ b/src/babashka/impl/async.clj @@ -1,18 +1,50 @@ (ns babashka.impl.async {:no-doc true} (:require [clojure.core.async :as async] - [clojure.core.async.impl.protocols :as protocols])) + [clojure.core.async.impl.protocols :as protocols] + [sci.impl.vars :as vars])) + +(def ^java.util.concurrent.Executor executor @#'async/thread-macro-executor) + +(defn thread-call + "Executes f in another thread, returning immediately to the calling + thread. Returns a channel which will receive the result of calling + f when completed, then close." + [f] + (let [c (async/chan 1)] + (let [binds (vars/get-thread-binding-frame)] + (.execute executor + (fn [] + (vars/reset-thread-binding-frame binds) + (try + (let [ret (f)] + (when-not (nil? ret) + (async/>!! c ret))) + (finally + (async/close! c)))))) + c)) (defn thread [_ _ & body] `(~'clojure.core.async/thread-call (fn [] ~@body))) +(defn alt!! + "Like alt!, except as if by alts!!, will block until completed, and + not intended for use in (go ...) blocks." + [_ _ & clauses] + (async/do-alt 'clojure.core.async/alts!! clauses)) + +(defn go-loop + [_ _ bindings & body] + (list 'clojure.core.async/thread (list* 'loop bindings body))) + (def async-namespace {'!! async/>!! 'admix async/admix 'alts! async/alts! 'alts!! async/alts!! + 'alt!! (with-meta alt!! {:sci/macro true}) 'buffer async/buffer 'chan async/chan 'close! async/close! @@ -53,7 +85,7 @@ 'take! async/take! 'tap async/tap 'thread (with-meta thread {:sci/macro true}) - 'thread-call async/thread-call + 'thread-call thread-call 'timeout async/timeout 'to-chan async/to-chan 'toggle async/toggle @@ -65,7 +97,13 @@ 'unsub async/unsub 'unsub-all async/unsub-all 'untap async/untap - 'untap-all async/untap-all}) + 'untap-all async/untap-all + ;; polyfill + 'go (with-meta thread {:sci/macro true}) + '! async/>!! + 'alt! (with-meta alt!! {:sci/macro true}) + 'go-loop (with-meta go-loop {:sci/macro true})}) (def async-protocols-namespace {'ReadPort protocols/ReadPort}) diff --git a/src/babashka/impl/bencode.clj b/src/babashka/impl/bencode.clj new file mode 100644 index 00000000..3d452eb6 --- /dev/null +++ b/src/babashka/impl/bencode.clj @@ -0,0 +1,11 @@ +(ns babashka.impl.bencode + {:no-doc true} + (:require [babashka.impl.bencode.core :as bencode] + [sci.impl.namespaces :refer [copy-var]] + [sci.impl.vars :as vars])) + +(def tns (vars/->SciNamespace 'bencode.core nil)) + +(def bencode-namespace + {'read-bencode (copy-var bencode/read-bencode tns) + 'write-bencode (copy-var bencode/write-bencode tns)}) diff --git a/src/babashka/impl/bencode/core.clj b/src/babashka/impl/bencode/core.clj new file mode 100644 index 00000000..20e24e5b --- /dev/null +++ b/src/babashka/impl/bencode/core.clj @@ -0,0 +1,420 @@ +(ns babashka.impl.bencode.core + "A netstring and bencode implementation for Clojure." + {:author "Meikel Brandmeyer" + :no-doc true} + (:require [clojure.java.io :as io]) + (:import [java.io ByteArrayOutputStream + EOFException + InputStream + IOException + OutputStream + PushbackInputStream])) + +;; Copyright (c) Meikel Brandmeyer. All rights reserved. +;; The use and distribution terms for this software are covered by the +;; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) +;; which can be found in the file epl-v10.html at the root of this distribution. +;; By using this software in any fashion, you are agreeing to be bound by +;; the terms of this license. +;; You must not remove this notice, or any other, from this software. + +;; # Motivation +;; +;; In each and every application, which contacts peer processes via some +;; communication channel, the handling of the communication channel is +;; obviously a central part of the application. Unfortunately introduces +;; handling of buffers of varying sizes often bugs in form of buffer +;; overflows and similar. +;; +;; A strong factor in this situation is of course the protocol which goes +;; over the wire. Depending on its design it might be difficult to estimate +;; the size of the input up front. This introduces more handling of message +;; buffers to accomodate for inputs of varying sizes. This is particularly +;; difficult in languages like C, where there is no bounds checking of array +;; accesses and where errors might go unnoticed for considerable amount of +;; time. +;; +;; To address these issues D. Bernstein developed the so called +;; [netstrings][net]. They are especially designed to allow easy construction +;; of the message buffers, easy and robust parsing. +;; +;; BitTorrent extended this to the [bencode][bc] protocol which also +;; includes ways to encode numbers and collections like lists or maps. +;; +;; *wire* is based on these ideas. +;; +;; [net]: http://cr.yp.to/proto/netstrings.txt +;; [bc]: http://wiki.theory.org/BitTorrentSpecification#Bencoding +;; +;; # Netstrings +;; +;; Now let's start with the basic netstrings. They consist of a byte count, +;; followed a colon and the binary data and a trailing comma. Examples: +;; +;; 13:Hello, World!, +;; 10:Guten Tag!, +;; 0:, +;; +;; The initial byte count allows to efficiently allocate a sufficiently +;; sized message buffer. The trailing comma serves as a hint to detect +;; incorrect netstrings. +;; +;; ## Low-level reading +;; +;; We will need some low-level reading helpers to read the bytes from +;; the input stream. These are `read-byte` as well as `read-bytes`. They +;; are split out, because doing such a simple task as reading a byte is +;; mild catastrophe in Java. So it would add some clutter to the algorithm +;; `read-netstring`. +;; +;; On the other hand they might be also useful elsewhere. +;; +;; To remove some magic numbers from the code below. + +(set! *warn-on-reflection* true) + +(def #^{:const true} i 105) +(def #^{:const true} l 108) +(def #^{:const true} d 100) +(def #^{:const true} comma 44) +(def #^{:const true} minus 45) + +;; These two are only used boxed. So we keep them extra here. + +(def e 101) +(def colon 58) + +(defn #^{:private true} read-byte + #^long [#^InputStream input] + (let [c (.read input)] + (when (neg? c) + (throw (EOFException. "Invalid netstring. Unexpected end of input."))) + ;; Here we have a quirk for example. `.read` returns -1 on end of + ;; input. However the Java `Byte` has only a range from -128 to 127. + ;; How does the fit together? + ;; + ;; The whole thing is shifted. `.read` actually returns an int + ;; between zero and 255. Everything below the value 128 stands + ;; for itself. But larger values are actually negative byte values. + ;; + ;; So we have to do some translation here. `Byte/byteValue` would + ;; do that for us, but we want to avoid boxing here. + (if (< 127 c) (- c 256) c))) + +(defn #^{:private true :tag "[B"} read-bytes + #^Object [#^InputStream input n] + (let [content (byte-array n)] + (loop [offset (int 0) + len (int n)] + (let [result (.read input content offset len)] + (when (neg? result) + (throw + (EOFException. + "Invalid netstring. Less data available than expected."))) + (when (not= result len) + (recur (+ offset result) (- len result))))) + content)) + +;; `read-long` is used for reading integers from the stream as well +;; as the byte count prefixes of byte strings. The delimiter is \: +;; for byte count prefixes and \e for integers. + +(defn #^{:private true} read-long + #^long [#^InputStream input delim] + (loop [n (long 0)] + ;; We read repeatedly a byte from the input… + (let [b (read-byte input)] + ;; …and stop at the delimiter. + (cond + (= b minus) (- (read-long input delim)) + (= b delim) n + :else (recur (+ (* n (long 10)) (- (long b) (long 48)))))))) + +;; ## Reading a netstring +;; +;; Let's dive straight into reading a netstring from an `InputStream`. +;; +;; For convenience we split the function into two subfunctions. The +;; public `read-netstring` is the normal entry point, which also checks +;; for the trailing comma after reading the payload data with the +;; private `read-netstring*`. +;; +;; The reason we need the less strict `read-netstring*` is that with +;; bencode we don't have a trailing comma. So a check would not be +;; beneficial here. +;; +;; However the consumer doesn't have to care. `read-netstring` as +;; well as `read-bencode` provide the public entry points, which do +;; the right thing. Although they both may reference the `read-netstring*` +;; underneath. +;; +;; With this in mind we define the inner helper function first. + +(declare #^"[B" string>payload + #^String stringpayload` and `stringpayload + [#^String s] + (.getBytes s "UTF-8")) + +(defn #^{:private true :tag String} stringpayload (str (alength content)))) + (.write (int colon)) + (.write content))) + +(defn write-netstring + "Write the given binary data to the output stream in form of a classic + netstring." + [#^OutputStream output content] + (doto output + (write-netstring* content) + (.write (int comma)))) + +;; # Bencode +;; +;; However most of the time we don't want to send simple blobs of data +;; back and forth. The data sent between the communication peers usually +;; have some structure, which has to be carried along the way to the +;; other side. Here [bencode][bc] come into play. +;; +;; Bencode defines additionally to netstrings easily parseable structures +;; for lists, maps and numbers. It allows to communicate information +;; about the data structure to the peer on the other side. +;; +;; ## Tokens +;; +;; The data is encoded in tokens in bencode. There are several types of +;; tokens: +;; +;; * A netstring without trailing comma for string data. +;; * A tag specifiyng the type of the following tokens. +;; The tag may be one of these: +;; * `\i` to encode integers. +;; * `\l` to encode lists of items. +;; * `\d` to encode maps of item pairs. +;; * `\e` to end the a previously started tag. +;; +;; ## Reading bencode +;; +;; Reading bencode encoded data is basically parsing a stream of tokens +;; from the input. Hence we need a read-token helper which allows to +;; retrieve the next token. + +(defn #^{:private true} read-token + [#^PushbackInputStream input] + (let [ch (read-byte input)] + (cond + (= (long e) ch) nil + (= i ch) :integer + (= l ch) :list + (= d ch) :map + :else (do + (.unread input (int ch)) + (read-netstring* input))))) + +;; To read the bencode encoded data we walk a long the sequence of tokens +;; and act according to the found tags. + +(declare read-integer read-list read-map) + +(defn read-bencode + "Read bencode token from the input stream." + [input] + (let [token (read-token input)] + (case token + :integer (read-integer input) + :list (read-list input) + :map (read-map input) + token))) + +;; Of course integers and the collection types are have to treated specially. +;; +;; Integers for example consist of a sequence of decimal digits. + +(defn #^{:private true} read-integer + [input] + (read-long input e)) + +;; *Note:* integers are an ugly special case, which cannot be +;; handled with `read-token` or `read-netstring*`. +;; +;; Lists are just a sequence of other tokens. + +(declare token-seq) + +(defn #^{:private true} read-list + [input] + (vec (token-seq input))) + +;; Maps are sequences of key/value pairs. The keys are always +;; decoded into strings. The values are kept as is. + +(defn #^{:private true} read-map + [input] + (->> (token-seq input) + (into {} (comp (partition-all 2) + (map (fn [[k v]] + [(string> #(read-bencode input) + repeatedly + (take-while identity))) + +;; ## Writing bencode +;; +;; Writing bencode is similar easy as reading it. The main entry point +;; takes a string, map, sequence or integer and writes it according to +;; the rules to the given OutputStream. + +(defmulti write-bencode + "Write the given thing to the output stream. “Thing” means here a + string, map, sequence or integer. Alternatively an ByteArray may + be provided whose contents are written as a bytestring. Similar + the contents of a given InputStream are written as a byte string. + Named things (symbols or keywords) are written in the form + 'namespace/name'." + (fn [_output thing] + (cond + (bytes? thing) :bytes + (instance? InputStream thing) :input-stream + (integer? thing) :integer + (string? thing) :string + (symbol? thing) :named + (keyword? thing) :named + (map? thing) :map + (or (nil? thing) (coll? thing) (.isArray (class thing))) :list + :else (type thing)))) + +(defmethod write-bencode :default + [output x] + (throw (IllegalArgumentException. (str "Cannot write value of type " (class x))))) + +;; The following methods should be pretty straight-forward. +;; +;; The easiest case is of course when we already have a byte array. +;; We can simply pass it on to the underlying machinery. + +(defmethod write-bencode :bytes + [output bytes] + (write-netstring* output bytes)) + +;; For strings we simply write the string as a netstring without +;; trailing comma after encoding the string as UTF-8 bytes. + +(defmethod write-bencode :string + [output string] + (write-netstring* output (string>payload string))) + +;; Streaming does not really work, since we need to know the +;; number of bytes to write upfront. So we read in everything +;; for InputStreams and pass on the byte array. + +(defmethod write-bencode :input-stream + [output stream] + (let [bytes (ByteArrayOutputStream.)] + (io/copy stream bytes) + (write-netstring* output (.toByteArray bytes)))) + +;; Integers are again the ugly special case. + +(defmethod write-bencode :integer + [#^OutputStream output n] + (doto output + (.write (int i)) + (.write (string>payload (str n))) + (.write (int e)))) + +;; Symbols and keywords are converted to a string of the +;; form 'namespace/name' or just 'name' in case its not +;; qualified. We do not add colons for keywords since the +;; other side might not have the notion of keywords. + +(defmethod write-bencode :named + [output thing] + (let [nspace (namespace thing) + name (name thing)] + (->> (str (when nspace (str nspace "/")) name) + string>payload + (write-netstring* output)))) + +;; Lists as well as maps work recursively to print their elements. + +(defmethod write-bencode :list + [#^OutputStream output lst] + (.write output (int l)) + (doseq [elt lst] + (write-bencode output elt)) + (.write output (int e))) + +;; However, maps are a bit special because their keys are sorted +;; lexicographically based on their byte string represantation. + +(declare lexicographically) + +(defmethod write-bencode :map + [#^OutputStream output m] + (let [translation (into {} (map (juxt string>payload identity) (keys m))) + key-strings (sort lexicographically (keys translation)) + >value (comp m translation)] + (.write output (int d)) + (doseq [k key-strings] + (write-netstring* output k) + (write-bencode output (>value k))) + (.write output (int e)))) + +;; However, since byte arrays are not `Comparable` we need a custom +;; comparator which we can feed to `sort`. + +(defn #^{:private true} lexicographically + [#^"[B" a #^"[B" b] + (let [alen (alength a) + blen (alength b) + len (min alen blen)] + (loop [i 0] + (if (== i len) + (- alen blen) + (let [x (- (int (aget a i)) (int (aget b i)))] + (if (zero? x) + (recur (inc i)) + x)))))) diff --git a/src/babashka/impl/classes.clj b/src/babashka/impl/classes.clj index c6696613..43308368 100644 --- a/src/babashka/impl/classes.clj +++ b/src/babashka/impl/classes.clj @@ -1,19 +1,11 @@ (ns babashka.impl.classes {:no-doc true} (:require - [cheshire.core :as json] - #_[clojure.string :as str])) - -;; (def os-name (str/lower-case (System/getProperty "os.name"))) -;; (def os (cond (str/includes? os-name "mac") :mac -;; (or (str/includes? os-name "nix") -;; (str/includes? os-name "nux")) :linux -;; (str/includes? os-name "win") :windows)) -;; (def unix-like? (or (identical? os :linux) -;; (identical? os :mac))) + [cheshire.core :as json])) (def classes - '{:all [java.io.BufferedReader + `{:all [clojure.lang.ExceptionInfo + java.io.BufferedReader java.io.BufferedWriter java.io.ByteArrayInputStream java.io.ByteArrayOutputStream @@ -21,19 +13,27 @@ java.io.InputStream java.io.IOException java.io.OutputStream + java.io.FileReader + java.io.PushbackInputStream java.io.Reader + java.io.SequenceInputStream java.io.StringReader java.io.StringWriter java.io.Writer java.lang.ArithmeticException java.lang.AssertionError java.lang.Boolean + java.lang.Byte + java.lang.Comparable java.lang.Class java.lang.Double java.lang.Exception java.lang.Integer java.lang.Long + java.lang.NumberFormatException java.lang.Math + java.lang.Runtime + java.lang.RuntimeException java.util.concurrent.LinkedBlockingQueue java.lang.Object java.lang.String @@ -44,15 +44,21 @@ java.lang.ProcessBuilder java.lang.ProcessBuilder$Redirect java.math.BigInteger - java.net.URI + java.net.DatagramSocket + java.net.DatagramPacket java.net.HttpURLConnection + java.net.InetAddress java.net.ServerSocket java.net.Socket java.net.UnknownHostException + java.net.URI + ;; java.net.URL, see below java.net.URLEncoder java.net.URLDecoder java.nio.file.CopyOption java.nio.file.FileAlreadyExistsException + java.nio.file.FileSystem + java.nio.file.FileSystems java.nio.file.Files java.nio.file.LinkOption java.nio.file.NoSuchFileException @@ -83,18 +89,24 @@ java.time.ZonedDateTime java.time.ZoneId java.time.ZoneOffset + java.time.temporal.ChronoUnit java.time.temporal.TemporalAccessor java.util.regex.Pattern java.util.Base64 java.util.Base64$Decoder java.util.Base64$Encoder java.util.Date + java.util.MissingResourceException + java.util.Properties java.util.UUID java.util.concurrent.TimeUnit java.util.zip.InflaterInputStream java.util.zip.DeflaterInputStream java.util.zip.GZIPInputStream - java.util.zip.GZIPOutputStream] + java.util.zip.GZIPOutputStream + org.yaml.snakeyaml.error.YAMLException + ~(symbol "[B") + ] :constructors [clojure.lang.Delay clojure.lang.MapEntry clojure.lang.LineNumberingPushbackReader @@ -104,8 +116,7 @@ :methods [borkdude.graal.LockFix ;; support for locking ] :fields [clojure.lang.PersistentQueue] - :instance-checks [clojure.lang.ExceptionInfo - clojure.lang.IObj + :instance-checks [clojure.lang.IObj clojure.lang.IEditableCollection] :custom {clojure.lang.LineNumberingPushbackReader {:allPublicConstructors true :allPublicMethods true} @@ -197,7 +208,13 @@ (instance? java.io.ByteArrayOutputStream v) java.io.ByteArrayOutputStream (instance? java.security.MessageDigest v) - java.security.MessageDigest))))) + java.security.MessageDigest + (instance? java.io.InputStream v) + java.io.InputStream + (instance? java.io.OutputStream v) + java.io.OutputStream + (instance? java.nio.file.FileSystem v) + java.nio.file.FileSystem))))) (def class-map (gen-class-map)) diff --git a/src/babashka/impl/clojure/core.clj b/src/babashka/impl/clojure/core.clj index bb7502af..c7e9452a 100644 --- a/src/babashka/impl/clojure/core.clj +++ b/src/babashka/impl/clojure/core.clj @@ -1,7 +1,9 @@ (ns babashka.impl.clojure.core {:no-doc true} - (:refer-clojure :exclude [future]) - (:require [borkdude.graal.locking :as locking])) + (:refer-clojure :exclude [future read read-string]) + (:require [borkdude.graal.locking :as locking] + [sci.core :as sci] + [sci.impl.namespaces :refer [copy-core-var]])) (defn locking* [form bindings v f & args] (apply @#'locking/locking form bindings v f args)) @@ -16,17 +18,17 @@ ret#)) (def core-extras - {'file-seq file-seq - 'agent agent - 'instance? instance? ;; TODO: move to sci - 'send send - 'send-off send-off - 'promise promise - 'deliver deliver + {'file-seq (copy-core-var file-seq) + 'agent (copy-core-var agent) + 'send (copy-core-var send) + 'send-off (copy-core-var send-off) + 'promise (copy-core-var promise) + 'deliver (copy-core-var deliver) 'locking (with-meta locking* {:sci/macro true}) - 'shutdown-agents shutdown-agents - 'slurp slurp - 'spit spit + 'shutdown-agents (copy-core-var shutdown-agents) + 'slurp (copy-core-var slurp) + 'spit (copy-core-var spit) 'time (with-meta time* {:sci/macro true}) - 'Throwable->map Throwable->map - 'compare-and-set! compare-and-set!}) + 'Throwable->map (copy-core-var Throwable->map) + 'compare-and-set! (copy-core-var compare-and-set!) + '*data-readers* (sci/new-dynamic-var '*data-readers* nil)}) diff --git a/src/babashka/impl/clojure/core/server.clj b/src/babashka/impl/clojure/core/server.clj index 113934a0..3de2f97e 100644 --- a/src/babashka/impl/clojure/core/server.clj +++ b/src/babashka/impl/clojure/core/server.clj @@ -1,5 +1,5 @@ -;; Modified / stripped version of clojure.core.server for use with babashka on -;; GraalVM. +;; Modified / stripped version of clojure.core.server for use with babashka on +;; GraalVM. ;; ;; Copyright (c) Rich Hickey. All rights reserved. ;; The use and distribution terms for this software are covered by the diff --git a/src/babashka/impl/clojure/main.clj b/src/babashka/impl/clojure/main.clj index 3d8a9f2f..db26af93 100644 --- a/src/babashka/impl/clojure/main.clj +++ b/src/babashka/impl/clojure/main.clj @@ -48,6 +48,11 @@ *e nil] ~@body)) +(def ^{:doc "A sequence of lib specs that are applied to `require` +by default when a new command-line REPL is started."} repl-requires + '[[clojure.repl :refer (dir doc)] + [clojure.pprint :refer (pprint)]]) + (defn repl "Generic, reusable, read-eval-print loop. By default, reads from *in*, writes to *out*, and prints exception summaries to *err*. If you use the diff --git a/src/babashka/impl/clojure/pprint.clj b/src/babashka/impl/clojure/pprint.clj new file mode 100644 index 00000000..3d2a3695 --- /dev/null +++ b/src/babashka/impl/clojure/pprint.clj @@ -0,0 +1,12 @@ +(ns babashka.impl.clojure.pprint + {:no-doc true} + (:require [fipp.edn :as fipp])) + +(defn pprint + ([edn] + (fipp/pprint edn)) + ([edn writer] + (fipp/pprint edn {:writer writer}))) + +(def pprint-namespace + {'pprint pprint}) diff --git a/src/babashka/impl/clojure/stacktrace.clj b/src/babashka/impl/clojure/stacktrace.clj index 96fd9f32..07f19ef1 100644 --- a/src/babashka/impl/clojure/stacktrace.clj +++ b/src/babashka/impl/clojure/stacktrace.clj @@ -1,88 +1,16 @@ -;; Copyright (c) Rich Hickey. All rights reserved. -;; The use and distribution terms for this software are covered by the -;; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) -;; which can be found in the file epl-v10.html at the root of this distribution. -;; By using this software in any fashion, you are agreeing to be bound by -;; the terms of this license. -;; You must not remove this notice, or any other, from this software. +(ns babashka.impl.clojure.stacktrace + {:no-doc true} + (:require [clojure.stacktrace :as stacktrace] + [sci.core :as sci])) -;;; stacktrace.clj: print Clojure-centric stack traces - -;; by Stuart Sierra -;; January 6, 2009 - -(ns ^{:doc "Print stack traces oriented towards Clojure, not Java." - :author "Stuart Sierra" - :no-doc true} - babashka.impl.clojure.stacktrace) - -(set! *warn-on-reflection* true) - -(defn root-cause - "Returns the last 'cause' Throwable in a chain of Throwables." - {:added "1.1"} - [^Throwable tr] - (if-let [cause (.getCause tr)] - (recur cause) - tr)) - -(defn print-trace-element - "Prints a Clojure-oriented view of one element in a stack trace." - {:added "1.1"} - [^StackTraceElement e] - (let [class (.getClassName e) - method (.getMethodName e)] - (let [match (re-matches #"^([A-Za-z0-9_.-]+)\$(\w+)__\d+$" (str class))] - (if (and match (= "invoke" method)) - (apply printf "%s/%s" (rest match)) - (printf "%s.%s" class method)))) - (printf " (%s:%d)" (or (.getFileName e) "") (.getLineNumber e))) - -(defn print-throwable - "Prints the class and message of a Throwable. Prints the ex-data map - if present." - {:added "1.1"} - [^Throwable tr] - (printf "%s: %s" (.getName (class tr)) (.getMessage tr)) - (when-let [info (ex-data tr)] - (newline) - (pr info))) - -(defn print-stack-trace - "Prints a Clojure-oriented stack trace of tr, a Throwable. - Prints a maximum of n stack frames (default: unlimited). - Does not print chained exceptions (causes)." - {:added "1.1"} - ([tr] (print-stack-trace tr nil)) - ([^Throwable tr n] - (let [st (.getStackTrace tr)] - (print-throwable tr) - (newline) - (print " at ") - (if-let [e (first st)] - (print-trace-element e) - (print "[empty stack trace]")) - (newline) - (doseq [e (if (nil? n) - (rest st) - (take (dec n) (rest st)))] - (print " ") - (print-trace-element e) - (newline))))) - -(defn print-cause-trace - "Like print-stack-trace but prints chained exceptions (causes)." - {:added "1.1"} - ([tr] (print-cause-trace tr nil)) - ([^Throwable tr n] - (print-stack-trace tr n) - (when-let [cause (.getCause tr)] - (print "Caused by: " ) - (recur cause n)))) +(defmacro wrap-out [f] + `(fn [& ~'args] + (binding [*out* @sci/out] + (apply ~f ~'args)))) (def stacktrace-namespace - {'root-cause root-cause - 'print-trace-element print-trace-element - 'print-throwable print-throwable - 'print-stack-trace print-stack-trace - 'print-cause-trace print-cause-trace}) + {'root-cause stacktrace/root-cause + 'print-trace-element (wrap-out stacktrace/print-trace-element) + 'print-throwable (wrap-out stacktrace/print-throwable) + 'print-stack-trace (wrap-out stacktrace/print-stack-trace) + 'print-cause-trace (wrap-out stacktrace/print-cause-trace)}) diff --git a/src/babashka/impl/clojure/test.clj b/src/babashka/impl/clojure/test.clj index 60fcec78..97406f5d 100644 --- a/src/babashka/impl/clojure/test.clj +++ b/src/babashka/impl/clojure/test.clj @@ -1,10 +1,10 @@ - ; Copyright (c) Rich Hickey. All rights reserved. - ; The use and distribution terms for this software are covered by the - ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) - ; which can be found in the file epl-v10.html at the root of this distribution. - ; By using this software in any fashion, you are agreeing to be bound by - ; the terms of this license. - ; You must not remove this notice, or any other, from this software. +;; Copyright (c) Rich Hickey. All rights reserved. +;; The use and distribution terms for this software are covered by the +;; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) +;; which can be found in the file epl-v10.html at the root of this distribution. +;; By using this software in any fashion, you are agreeing to be bound by +;; the terms of this license. +;; You must not remove this notice, or any other, from this software. ;;; test.clj: test framework for Clojure @@ -232,9 +232,8 @@ For additional event types, see the examples in the code. "} babashka.impl.clojure.test - (:require [babashka.impl.clojure.stacktrace :as stack] - [babashka.impl.common :refer [ctx]] - [clojure.string :as str] + (:require [babashka.impl.common :refer [ctx]] + [clojure.stacktrace :as stack] [clojure.template :as temp] [sci.core :as sci] [sci.impl.analyzer :as ana] @@ -430,9 +429,9 @@ result# (apply ~pred values#)] (if result# (clojure.test/do-report {:type :pass, :message ~msg, - :expected '~form, :actual (cons ~pred values#)}) + :expected '~form, :actual (cons ~pred values#)}) (clojure.test/do-report {:type :fail, :message ~msg, - :expected '~form, :actual (list '~'not (cons '~pred values#))})) + :expected '~form, :actual (list '~'not (cons '~pred values#))})) result#))) (defn assert-any @@ -443,9 +442,9 @@ `(let [value# ~form] (if value# (clojure.test/do-report {:type :pass, :message ~msg, - :expected '~form, :actual value#}) + :expected '~form, :actual value#}) (clojure.test/do-report {:type :fail, :message ~msg, - :expected '~form, :actual value#})) + :expected '~form, :actual value#})) value#)) @@ -479,9 +478,9 @@ (let [result# (instance? klass# object#)] (if result# (clojure.test/do-report {:type :pass, :message ~msg, - :expected '~form, :actual (class object#)}) + :expected '~form, :actual (class object#)}) (clojure.test/do-report {:type :fail, :message ~msg, - :expected '~form, :actual (class object#)})) + :expected '~form, :actual (class object#)})) result#))) (defmethod assert-expr 'thrown? [msg form] @@ -492,10 +491,10 @@ body (nthnext form 2)] `(try ~@body (clojure.test/do-report {:type :fail, :message ~msg, - :expected '~form, :actual nil}) + :expected '~form, :actual nil}) (catch ~klass e# (clojure.test/do-report {:type :pass, :message ~msg, - :expected '~form, :actual e#}) + :expected '~form, :actual e#}) e#)))) (defmethod assert-expr 'thrown-with-msg? [msg form] @@ -512,7 +511,7 @@ (let [m# (.getMessage e#)] (if (re-find ~re m#) (clojure.test/do-report {:type :pass, :message ~msg, - :expected '~form, :actual e#}) + :expected '~form, :actual e#}) (clojure.test/do-report {:type :fail, :message ~msg, :expected '~form, :actual e#}))) e#)))) diff --git a/src/babashka/impl/curl.clj b/src/babashka/impl/curl.clj new file mode 100644 index 00000000..cf24bc60 --- /dev/null +++ b/src/babashka/impl/curl.clj @@ -0,0 +1,16 @@ +(ns babashka.impl.curl + {:no-doc true} + (:require [babashka.curl :as curl] + [sci.impl.namespaces :refer [copy-var]] + [sci.impl.vars :as vars])) + +(def tns (vars/->SciNamespace 'babashka.curl nil)) + +(def curl-namespace + {'request (copy-var curl/request tns) + 'get (copy-var curl/get tns) + 'patch (copy-var curl/patch tns) + 'post (copy-var curl/post tns) + 'put (copy-var curl/put tns) + 'head (copy-var curl/head tns) + 'curl-command (copy-var curl/curl-command tns)}) diff --git a/src/babashka/impl/nrepl_server.clj b/src/babashka/impl/nrepl_server.clj new file mode 100644 index 00000000..7e1fbf4d --- /dev/null +++ b/src/babashka/impl/nrepl_server.clj @@ -0,0 +1,198 @@ +(ns babashka.impl.nrepl-server + {:no-doc true} + (:refer-clojure :exclude [send future binding]) + (:require [babashka.impl.bencode.core :refer [read-bencode]] + [babashka.impl.nrepl-server.utils :refer [dev? response-for send send-exception + replying-print-writer]] + [clojure.string :as str] + [clojure.tools.reader.reader-types :as r] + [sci.core :as sci] + [sci.impl.interpreter :refer [eval-string* eval-form]] + [sci.impl.parser :as p] + [sci.impl.utils :as sci-utils] + [sci.impl.vars :as vars]) + (:import [java.io InputStream PushbackInputStream EOFException BufferedOutputStream] + [java.net ServerSocket])) + +(set! *warn-on-reflection* true) + +(defn eval-msg [ctx o msg] + (try + (let [code-str (get msg :code) + reader (r/indexing-push-back-reader (r/string-push-back-reader code-str)) + ns-str (get msg :ns) + sci-ns (when ns-str (sci-utils/namespace-object (:env ctx) (symbol ns-str) true nil))] + (when @dev? (println "current ns" (vars/current-ns-name))) + (sci/with-bindings (cond-> {} + sci-ns (assoc vars/current-ns sci-ns)) + (loop [] + (let [pw (replying-print-writer o msg) + form (p/parse-next ctx reader) + value (if (identical? :edamame.impl.parser/eof form) ::nil + (sci/with-bindings {sci/out pw} + (eval-form ctx form))) + env (:env ctx)] + (swap! env update-in [:namespaces 'clojure.core] + (fn [core] + (assoc core + '*1 value + '*2 (get core '*1) + '*3 (get core '*2)))) + (send o (response-for msg (cond-> {"ns" (vars/current-ns-name)} + (not (identical? value ::nil)) (assoc "value" (pr-str value))))) + (when (not (identical? ::nil value)) + (recur))))) + (send o (response-for msg {"status" #{"done"}}))) + (catch Exception ex + (swap! (:env ctx) update-in [:namespaces 'clojure.core] + assoc '*e ex) + (send-exception o msg ex)))) + +(defn fully-qualified-syms [ctx ns-sym] + (let [syms (eval-string* ctx (format "(keys (ns-map '%s))" ns-sym)) + sym-strs (map #(str "`" %) syms) + sym-expr (str "[" (str/join " " sym-strs) "]") + syms (eval-string* ctx sym-expr)] + syms)) + +(defn match [_alias->ns ns->alias query [sym-ns sym-name qualifier]] + (let [pat (re-pattern query)] + (or (when (and (identical? :unqualified qualifier) (re-find pat sym-name)) + [sym-ns sym-name]) + (when sym-ns + (or (when (re-find pat (str (get ns->alias (symbol sym-ns)) "/" sym-name)) + [sym-ns (str (get ns->alias (symbol sym-ns)) "/" sym-name)]) + (when (re-find pat (str sym-ns "/" sym-name)) + [sym-ns (str sym-ns "/" sym-name)])))))) + +(defn complete [ctx o msg] + (try + (let [ns-str (get msg :ns) + sci-ns (when ns-str + (sci-utils/namespace-object (:env ctx) (symbol ns-str) nil false))] + (sci/binding [vars/current-ns (or sci-ns @vars/current-ns)] + (let [query (:symbol msg) + from-current-ns (fully-qualified-syms ctx (eval-string* ctx "(ns-name *ns*)")) + from-current-ns (map (fn [sym] + [(namespace sym) (name sym) :unqualified]) + from-current-ns) + alias->ns (eval-string* ctx "(let [m (ns-aliases *ns*)] (zipmap (keys m) (map ns-name (vals m))))") + ns->alias (zipmap (vals alias->ns) (keys alias->ns)) + from-aliased-nss (doall (mapcat + (fn [alias] + (let [ns (get alias->ns alias) + syms (eval-string* ctx (format "(keys (ns-publics '%s))" ns))] + (map (fn [sym] + [(str ns) (str sym) :qualified]) + syms))) + (keys alias->ns))) + svs (concat from-current-ns from-aliased-nss) + completions (keep (fn [entry] + (match alias->ns ns->alias query entry)) + svs) + completions (mapv (fn [[namespace name]] + {"candidate" (str name) "ns" (str namespace) #_"type" #_"function"}) + completions)] + (when @dev? (prn "completions" completions)) + (send o (response-for msg {"completions" completions + "status" #{"done"}}))))) + (catch Throwable e + (println e) + (send o (response-for msg {"completions" [] + "status" #{"done"}}))))) + +(defn close-session [ctx msg _is os] + (let [session (:session msg)] + (swap! (:sessions ctx) disj session)) + (send os (response-for msg {"status" #{"done" "session-closed"}}))) + +(defn ls-sessions [ctx msg os] + (let [sessions @(:sessions ctx)] + (send os (response-for msg {"sessions" sessions + "status" #{"done"}})))) + +(defn read-msg [msg] + (-> (zipmap (map keyword (keys msg)) + (map #(if (bytes? %) + (String. (bytes %)) + %) (vals msg))) + (update :op keyword))) + +(defn session-loop [ctx ^InputStream is os id] + (when @dev? (println "Reading!" id (.available is))) + (when-let [msg (try (read-bencode is) + (catch EOFException _ + (println "Client closed connection.")))] + (let [msg (read-msg msg)] + (when @dev? (prn "Received" msg)) + (case (get msg :op) + :clone (do + (when @dev? (println "Cloning!")) + (let [id (str (java.util.UUID/randomUUID))] + (swap! (:sessions ctx) (fnil conj #{}) id) + (send os (response-for msg {"new-session" id "status" #{"done"}})) + (recur ctx is os id))) + :close (do (close-session ctx msg is os) + (recur ctx is os id)) + :eval (do + (eval-msg ctx os msg) + (recur ctx is os id)) + :load-file (let [file (:file msg) + msg (assoc msg :code file)] + (eval-msg ctx os msg) + (recur ctx is os id)) + :complete (do + (complete ctx os msg) + (recur ctx is os id)) + :describe + (do (send os (response-for msg {"status" #{"done"} + "ops" (zipmap #{"clone" "close" "eval" "load-file" + "complete" "describe" "ls-sessions"} + (repeat {}))})) + (recur ctx is os id)) + :ls-sessions (do (ls-sessions ctx msg os) + (recur ctx is os id)) + ;; fallback + (do (when @dev? + (println "Unhandled message" msg)) + (send os (response-for msg {"status" #{"error" "unknown-op" "done"}})) + (recur ctx is os id)))))) + +(defn listen [ctx ^ServerSocket listener] + (when @dev? (println "Listening")) + (let [client-socket (.accept listener) + in (.getInputStream client-socket) + in (PushbackInputStream. in) + out (.getOutputStream client-socket) + out (BufferedOutputStream. out)] + (when @dev? (println "Connected.")) + (sci/future + (sci/binding + ;; allow *ns* to be set! inside future + [vars/current-ns (vars/->SciNamespace 'user nil) + sci/print-length @sci/print-length] + (session-loop ctx in out "pre-init"))) + (recur ctx listener))) + +(def server (atom nil)) + +(defn stop-server! [] + (when-let [s @server] + (.close ^ServerSocket s) + (reset! server nil))) + +(defn start-server! [ctx host+port] + (vreset! dev? (= "true" (System/getenv "BABASHKA_DEV"))) + (let [ctx (assoc ctx :sessions (atom #{})) + parts (str/split host+port #":") + [address port] (if (= 1 (count parts)) + [nil (Integer. ^String (first parts))] + [(java.net.InetAddress/getByName (first parts)) + (Integer. ^String (second parts))]) + host+port (if-not address (str "localhost:" port) + host+port) + socket-server (new ServerSocket port 0 address)] + (println "Started nREPL server at" host+port) + (println "For more info visit https://github.com/borkdude/babashka/blob/master/doc/repl.md#nrepl.") + (reset! server socket-server) + (listen ctx socket-server))) diff --git a/src/babashka/impl/nrepl_server/utils.clj b/src/babashka/impl/nrepl_server/utils.clj new file mode 100644 index 00000000..46dca89f --- /dev/null +++ b/src/babashka/impl/nrepl_server/utils.clj @@ -0,0 +1,63 @@ +(ns babashka.impl.nrepl-server.utils + {:no-doc true} + (:refer-clojure :exclude [send]) + (:require [babashka.impl.bencode.core :refer [write-bencode]]) + (:import [java.io Writer PrintWriter StringWriter OutputStream BufferedWriter])) + +(set! *warn-on-reflection* true) + +(def dev? (volatile! nil)) + +(defn response-for [old-msg msg] + (let [session (get old-msg :session "none") + id (get old-msg :id "unknown")] + (assoc msg "session" session "id" id))) + +(defn send [^OutputStream os msg] + ;;(when @dev? (prn "Sending" msg)) + (write-bencode os msg) + (.flush os)) + +(defn send-exception [os msg ^Throwable ex] + (let [ex-map (Throwable->map ex) + ex-name (-> ex-map :via first :type) + cause (:cause ex-map)] + (when @dev? (prn "sending exception" ex-map)) + (send os (response-for msg {"err" (str ex-name ": " cause "\n")})) + (send os (response-for msg {"ex" (str "class " ex-name) + "root-ex" (str "class " ex-name) + "status" #{"eval-error"}})) + (send os (response-for msg {"status" #{"done"}})))) + +;; from https://github.com/nrepl/nrepl/blob/1cc9baae631703c184894559a2232275dc50dff6/src/clojure/nrepl/middleware/print.clj#L63 +(defn- to-char-array + ^chars + [x] + (cond + (string? x) (.toCharArray ^String x) + (integer? x) (char-array [(char x)]) + :else x)) + +;; from https://github.com/nrepl/nrepl/blob/1cc9baae631703c184894559a2232275dc50dff6/src/clojure/nrepl/middleware/print.clj#L99 +(defn replying-print-writer + "Returns a `java.io.PrintWriter` suitable for binding as `*out*` or `*err*`. All + of the content written to that `PrintWriter` will be sent as messages on the + transport of `msg`, keyed by `key`." + ^java.io.PrintWriter + [o msg] + (-> (proxy [Writer] [] + (write + ([x] + (let [cbuf (to-char-array x)] + (.write ^Writer this cbuf (int 0) (count cbuf)))) + ([x off len] + (let [cbuf (to-char-array x) + text (str (doto (StringWriter.) + (.write cbuf ^int off ^int len)))] + (when (pos? (count text)) + (when @dev? (println "out str:" text)) + (send o (response-for msg {"out" text})))))) + (flush []) + (close [])) + (BufferedWriter. 1024) + (PrintWriter. true))) diff --git a/src/babashka/impl/pipe_signal_handler.clj b/src/babashka/impl/pipe_signal_handler.clj index aee99151..e1bf83b2 100644 --- a/src/babashka/impl/pipe_signal_handler.clj +++ b/src/babashka/impl/pipe_signal_handler.clj @@ -9,8 +9,9 @@ (identical? :PIPE @pipe-state)) (defn handle-pipe! [] - (Signal/handle - (Signal. "PIPE") - (reify SignalHandler - (handle [_ _] - (vreset! pipe-state :PIPE))))) + (when-not (= "true" (System/getenv "BABASHKA_DISABLE_PIPE_SIGNAL_HANDLER")) + (Signal/handle + (Signal. "PIPE") + (reify SignalHandler + (handle [_ _] + (vreset! pipe-state :PIPE)))))) diff --git a/src/babashka/impl/repl.clj b/src/babashka/impl/repl.clj index 12fb91a6..9e2674c8 100644 --- a/src/babashka/impl/repl.clj +++ b/src/babashka/impl/repl.clj @@ -5,17 +5,21 @@ [clojure.java.io :as io] [clojure.string :as str] [clojure.tools.reader.reader-types :as r] - [sci.impl.interpreter :refer [eval-form]] - [sci.impl.parser :as parser] - [sci.impl.vars :as vars] [sci.core :as sci] - [sci.impl.io :as sio])) + [sci.impl.interpreter :refer [eval-form]] + [sci.impl.io :as sio] + [sci.impl.parser :as parser] + [sci.impl.vars :as vars])) + +(set! *warn-on-reflection* true) (defn repl-caught "Default :caught hook for repl" - [e] + [^Throwable e] (sci/with-bindings {sci/out @sci/err} - (sio/println (.getMessage ^Exception e)) + (sio/println (str (.. e getClass getName) + (when-let [m (.getMessage e)] + (str ": " m)) )) (sio/flush))) (defn repl @@ -31,19 +35,15 @@ (sio/println "Use :repl/quit or :repl/exit to quit the REPL.") (sio/println "Clojure rocks, Bash reaches.") (sio/println) - (eval-form sci-ctx '(require '[clojure.repl :refer [dir doc]])))) + (eval-form sci-ctx '(use 'clojure.repl)))) :read (or read (fn [_request-prompt request-exit] - ;; (prn "PEEK" @sci/in (r/peek-char @sci/in)) - ;; (prn "PEEK" @sci/in (r/peek-char @sci/in)) this works fine - (if (r/peek-char in) ;; if this is nil, we reached EOF - (let [v (parser/parse-next sci-ctx in)] - (if (or (identical? :repl/quit v) - (identical? :repl/exit v) - (identical? :edamame.impl.parser/eof v)) - request-exit - v)) - request-exit))) + (let [v (parser/parse-next sci-ctx in)] + (if (or (identical? :repl/quit v) + (identical? :repl/exit v) + (identical? :edamame.impl.parser/eof v)) + request-exit + v)))) :eval (or eval (fn [expr] (let [ret (eval-form (update sci-ctx diff --git a/src/babashka/impl/sigint_handler.clj b/src/babashka/impl/sigint_handler.clj new file mode 100644 index 00000000..9708bc59 --- /dev/null +++ b/src/babashka/impl/sigint_handler.clj @@ -0,0 +1,14 @@ +(ns babashka.impl.sigint-handler + {:no-doc true} + (:import [sun.misc Signal] + [sun.misc SignalHandler])) + +(set! *warn-on-reflection* true) + +(defn handle-sigint! [] + (Signal/handle + (Signal. "INT") + (reify SignalHandler + (handle [_ _] + ;; This is needed to run shutdown hooks on interrupt, System/exit triggers those + (System/exit 0))))) diff --git a/src/babashka/impl/transit.clj b/src/babashka/impl/transit.clj new file mode 100644 index 00000000..73eb7026 --- /dev/null +++ b/src/babashka/impl/transit.clj @@ -0,0 +1,13 @@ +(ns babashka.impl.transit + (:require [cognitect.transit :as transit] + [sci.impl.namespaces :refer [copy-var]] + [sci.impl.vars :as vars])) + + +(def tns (vars/->SciNamespace 'cognitect.transit nil)) + +(def transit-namespace + {'write (copy-var transit/write tns) + 'writer (copy-var transit/writer tns) + 'read (copy-var transit/read tns) + 'reader (copy-var transit/reader tns)}) diff --git a/src/babashka/impl/yaml.clj b/src/babashka/impl/yaml.clj new file mode 100644 index 00000000..33ad7a4f --- /dev/null +++ b/src/babashka/impl/yaml.clj @@ -0,0 +1,13 @@ +(ns babashka.impl.yaml + {:no-doc true} + (:require [clj-yaml.core :as yaml] + [sci.impl.namespaces :refer [copy-var]] + [sci.impl.vars :as vars])) + +(def yns (vars/->SciNamespace 'clj-yaml.core nil)) + +(def yaml-namespace + {'mark (copy-var yaml/mark yns) + 'unmark (copy-var yaml/unmark yns) + 'generate-string (copy-var yaml/generate-string yns) + 'parse-string (copy-var yaml/parse-string yns)}) diff --git a/src/babashka/main.clj b/src/babashka/main.clj index 77834a02..2faf9c00 100644 --- a/src/babashka/main.clj +++ b/src/babashka/main.clj @@ -2,31 +2,39 @@ {:no-doc true} (:require [babashka.impl.async :refer [async-namespace async-protocols-namespace]] + [babashka.impl.bencode :refer [bencode-namespace]] [babashka.impl.cheshire :refer [cheshire-core-namespace]] [babashka.impl.classes :as classes] [babashka.impl.classpath :as cp] [babashka.impl.clojure.core :refer [core-extras]] [babashka.impl.clojure.java.io :refer [io-namespace]] [babashka.impl.clojure.java.shell :refer [shell-namespace]] - [babashka.impl.clojure.main :refer [demunge]] - [babashka.impl.clojure.stacktrace :refer [stacktrace-namespace print-stack-trace]] + [babashka.impl.clojure.main :as clojure-main :refer [demunge]] + [babashka.impl.clojure.pprint :refer [pprint-namespace]] + [babashka.impl.clojure.stacktrace :refer [stacktrace-namespace]] [babashka.impl.common :as common] [babashka.impl.csv :as csv] + [babashka.impl.curl :refer [curl-namespace]] + [babashka.impl.nrepl-server :as nrepl-server] [babashka.impl.pipe-signal-handler :refer [handle-pipe! pipe-signal-received?]] [babashka.impl.repl :as repl] + [babashka.impl.sigint-handler :as sigint-handler] [babashka.impl.socket-repl :as socket-repl] [babashka.impl.test :as t] [babashka.impl.tools.cli :refer [tools-cli-namespace]] [babashka.impl.xml :as xml] + [babashka.impl.transit :refer [transit-namespace]] + [babashka.impl.yaml :refer [yaml-namespace]] [babashka.wait :as wait] [clojure.edn :as edn] [clojure.java.io :as io] + [clojure.stacktrace :refer [print-stack-trace]] [clojure.string :as str] - [fipp.edn :as fipp] [sci.addons :as addons] [sci.core :as sci] [sci.impl.interpreter :refer [eval-string*]] [sci.impl.opts :as sci-opts] + [sci.impl.types :as sci-types] [sci.impl.unrestrict :refer [*unrestricted*]] [sci.impl.vars :as vars]) (:gen-class)) @@ -110,7 +118,14 @@ (let [options (next options)] (recur (next options) (assoc opts-map - :socket-repl (first options)))) + :socket-repl (or (first options) + "1666")))) + ("--nrepl-server") + (let [options (next options)] + (recur (next options) + (assoc opts-map + :nrepl (or (first options) + "1667")))) ("--eval", "-e") (let [options (next options)] (recur (next options) @@ -154,7 +169,7 @@ (def usage-string "Usage: bb [ -i | -I ] [ -o | -O ] [ --stream ] [--verbose] [ ( --classpath | -cp ) ] [ --uberscript ] [ ( --main | -m ) | -e | -f | - --repl | --socket-repl [:] ] + --repl | --socket-repl [:] | --nrepl-server [:] ] [ arg* ]") (defn print-usage [] (println usage-string)) @@ -184,6 +199,7 @@ -m, --main Call the -main function from namespace with args. --repl Start REPL. Use rlwrap for history. --socket-repl Start socket REPL. Specify port (e.g. 1666) or host and port separated by colon (e.g. 127.0.0.1:1666). + --nrepl-server Start nREPL server. Specify port (e.g. 1667) or host and port separated by colon (e.g. 127.0.0.1:1667). --time Print execution time before exiting. -- Stop parsing args and pass everything after -- to *command-line-args* @@ -198,26 +214,27 @@ Everything after that is bound to *command-line-args*.")) (str/replace x #"^#!.*" "")) (throw (Exception. (str "File does not exist: " file)))))) -(defn read-edn [] - (edn/read {;;:readers *data-readers* - :eof ::EOF} *in*)) - (def reflection-var (sci/new-dynamic-var '*warn-on-reflection* false)) (defn load-file* [sci-ctx f] (let [f (io/file f) - s (slurp f)] + s (slurp f) + prev-ns @vars/current-ns] (sci/with-bindings {vars/current-file (.getCanonicalPath f)} - (eval-string* sci-ctx s)))) - -(defn eval* [sci-ctx form] - (eval-string* sci-ctx (pr-str form))) + (try + (eval-string* sci-ctx s) + (finally (sci-types/setVal vars/current-ns prev-ns)))))) (defn start-socket-repl! [address ctx] (socket-repl/start-repl! address ctx) ;; hang until SIGINT @(promise)) +(defn start-nrepl! [address ctx] + (nrepl-server/start-server! ctx address) + ;; hang until SIGINT + #_@(promise)) + (defn exit [n] (throw (ex-info "" {:bb/exit-code n}))) @@ -231,7 +248,11 @@ Everything after that is bound to *command-line-args*.")) async clojure.core.async csv clojure.data.csv json cheshire.core - xml clojure.data.xml}) + xml clojure.data.xml + yaml clj-yaml.core + curl babashka.curl + transit cognitect.transit + bencode bencode.core}) (def cp-state (atom nil)) @@ -257,12 +278,17 @@ Everything after that is bound to *command-line-args*.")) 'clojure.data.csv csv/csv-namespace 'cheshire.core cheshire-core-namespace 'clojure.stacktrace stacktrace-namespace - 'clojure.main {'demunge demunge} + 'clojure.main {'demunge demunge + 'repl-requires clojure-main/repl-requires} 'clojure.repl {'demunge demunge} 'clojure.test t/clojure-test-namespace 'babashka.classpath {'add-classpath add-classpath*} - 'clojure.pprint {'pprint fipp/pprint} - 'clojure.data.xml xml/xml-namespace}) + 'clojure.data.xml xml/xml-namespace + 'clj-yaml.core yaml-namespace + 'clojure.pprint pprint-namespace + 'babashka.curl curl-namespace + 'cognitect.transit transit-namespace + 'bencode.core bencode-namespace}) (def bindings {'java.lang.System/exit exit ;; override exit, so we have more control @@ -284,6 +310,7 @@ Everything after that is bound to *command-line-args*.")) (defn main [& args] (handle-pipe!) + (sigint-handler/handle-sigint!) #_(binding [*out* *err*] (prn "M" (meta (get bindings 'future)))) (binding [*unrestricted* true] @@ -293,16 +320,18 @@ Everything after that is bound to *command-line-args*.")) {:keys [:version :shell-in :edn-in :shell-out :edn-out :help? :file :command-line-args :expressions :stream? :time? - :repl :socket-repl + :repl :socket-repl :nrepl :verbose? :classpath :main :uberscript] :as _opts} (parse-opts args) + _ (when main (System/setProperty "babashka.main" main)) read-next (fn [*in*] (if (pipe-signal-received?) ::EOF (if stream? (if shell-in (or (read-line) ::EOF) - (read-edn)) + (edn/read {;;:readers *data-readers* + :eof ::EOF} *in*)) (delay (cond shell-in (shell-seq *in*) edn-in @@ -337,6 +366,7 @@ Everything after that is bound to *command-line-args*.")) :imports '{ArithmeticException java.lang.ArithmeticException AssertionError java.lang.AssertionError Boolean java.lang.Boolean + Byte java.lang.Byte Class java.lang.Class Double java.lang.Double Exception java.lang.Exception @@ -345,7 +375,10 @@ Everything after that is bound to *command-line-args*.")) File java.io.File Long java.lang.Long Math java.lang.Math + NumberFormatException java.lang.NumberFormatException Object java.lang.Object + Runtime java.lang.Runtime + RuntimeException java.lang.RuntimeException ProcessBuilder java.lang.ProcessBuilder String java.lang.String StringBuilder java.lang.StringBuilder @@ -357,17 +390,20 @@ Everything after that is bound to *command-line-args*.")) ctx (addons/future ctx) sci-ctx (sci-opts/init ctx) _ (vreset! common/ctx sci-ctx) + input-var (sci/new-dynamic-var '*input* nil) _ (swap! (:env sci-ctx) (fn [env] - (update-in env [:namespaces 'clojure.core] assoc - 'eval #(eval* sci-ctx %) - 'load-file #(load-file* sci-ctx %)))) - _ (swap! (:env sci-ctx) - (fn [env] - (assoc-in env [:namespaces 'clojure.main 'repl] - (fn [& opts] - (let [opts (apply hash-map opts)] - (repl/start-repl! sci-ctx opts)))))) + (update env :namespaces + (fn [namespaces] [:namespaces 'clojure.main 'repl] + (-> namespaces + (assoc-in ['clojure.core 'load-file] #(load-file* sci-ctx %)) + (assoc-in ['clojure.main 'repl] + (fn [& opts] + (let [opts (apply hash-map opts)] + (repl/start-repl! sci-ctx opts)))) + (assoc-in ['user (with-meta '*input* + (when-not stream? + {:sci.impl/deref! true}))] input-var)))))) preloads (some-> (System/getenv "BABASHKA_PRELOADS") (str/trim)) [expressions exit-code] (cond expressions [expressions nil] @@ -395,17 +431,16 @@ Everything after that is bound to *command-line-args*.")) [(print-help) 0] repl [(repl/start-repl! sci-ctx) 0] socket-repl [(start-socket-repl! socket-repl sci-ctx) 0] + nrepl [(start-nrepl! nrepl sci-ctx) 0] (not (str/blank? expression)) (try - (loop [in (read-next *in*)] - (let [_ (swap! env update-in [:namespaces 'user] - assoc (with-meta '*input* - (when-not stream? - {:sci.impl/deref! true})) - (sci/new-dynamic-var '*input* in))] + (loop [] + (let [in (read-next *in*)] (if (identical? ::EOF in) [nil 0] ;; done streaming - (let [res [(let [res (eval-string* sci-ctx expression)] + (let [res [(let [res + (sci/binding [input-var in] + (eval-string* sci-ctx expression))] (when (some? res) (if-let [pr-f (cond shell-out println edn-out prn)] @@ -416,7 +451,7 @@ Everything after that is bound to *command-line-args*.")) (pr-f res)) (prn res)))) 0]] (if stream? - (recur (read-next *in*)) + (recur) res))))) (catch Throwable e (error-handler* e verbose?))) @@ -440,7 +475,8 @@ Everything after that is bound to *command-line-args*.")) (defn -main [& args] (if-let [dev-opts (System/getenv "BABASHKA_DEV")] - (let [{:keys [:n]} (edn/read-string dev-opts) + (let [{:keys [:n]} (if (= "true" dev-opts) {:n 1} + (edn/read-string dev-opts)) last-iteration (dec n)] (dotimes [i n] (if (< i last-iteration) diff --git a/src/babashka/wait.clj b/src/babashka/wait.clj index af2cc9d4..f4bf6999 100644 --- a/src/babashka/wait.clj +++ b/src/babashka/wait.clj @@ -17,8 +17,8 @@ opts) t0 (System/currentTimeMillis)] (loop [] - (let [v (try (with-open [_ (Socket. host port)] - (- (System/currentTimeMillis) t0)) + (let [v (try (.close (Socket. host port)) + (- (System/currentTimeMillis) t0) (catch ConnectException _e (let [took (- (System/currentTimeMillis) t0)] (if (and timeout (>= took timeout)) diff --git a/test-resources/babashka/src_for_classpath_test/my/main2.clj b/test-resources/babashka/src_for_classpath_test/my/main2.clj new file mode 100644 index 00000000..49a3c7f3 --- /dev/null +++ b/test-resources/babashka/src_for_classpath_test/my/main2.clj @@ -0,0 +1,4 @@ +(ns my.main2) + +(defn -main [& _args] + (System/getProperty "babashka.main")) diff --git a/test-resources/babashka/statsd.clj b/test-resources/babashka/statsd.clj new file mode 100644 index 00000000..959fc3bf --- /dev/null +++ b/test-resources/babashka/statsd.clj @@ -0,0 +1,49 @@ +(ns statsd-client + "a simple StatsD client written in Clojure + + Usage: + statsd-client/increment 'foo + statsd-client/decrement 'foo + statsd-client/increment 'foo 1 + statsd-client/decrement 'foo 1 + statsd-client/gauge 'foo 1 + statsd-client/timing 'foo 1 + " + (:import (java.net InetAddress DatagramPacket DatagramSocket))) + +(def server-address "127.0.0.1") +(def server-port 8125) + +; UDP helper functions +(defn make-socket + ([] (new DatagramSocket)) + ([port] (new DatagramSocket port))) + +(defn send-data [send-socket ip port data] + (let [ipaddress (InetAddress/getByName ip), + send-packet (new DatagramPacket (.getBytes data) (.length data) ipaddress port)] + (.send send-socket send-packet))) + +(defn make-send [ip port] + (let [send-socket (make-socket)] + (fn [data] (send-data send-socket ip port data)))) + +(def send-msg (make-send server-address server-port)) + +; statsd client functions +(defn increment + ([metric] (increment metric 1)) + ([metric value] + (send-msg (str metric ":" value "|c")))) + +(defn decrement + ([metric] (increment metric -1)) + ([metric value] + (send-msg (str metric ":" value "|c")))) + +(defn timing [metric value] + (send-msg (str metric ":" value "|ms"))) + +(defn gauge [metric value] + (send-msg (str metric ":" value "|g"))) + diff --git a/test-resources/babashka/transit.clj b/test-resources/babashka/transit.clj new file mode 100644 index 00000000..3d3bda6a --- /dev/null +++ b/test-resources/babashka/transit.clj @@ -0,0 +1,18 @@ +(require '[cognitect.transit :as transit]) +(import [java.io ByteArrayInputStream ByteArrayOutputStream]) + +;; Write data to a stream +(def out (ByteArrayOutputStream. 4096)) +(def writer (transit/writer out :json)) +(transit/write writer "foo") +(transit/write writer {:a [1 2]}) + +;; Take a peek at the JSON +(.toString out) +;; => "{\"~#'\":\"foo\"} [\"^ \",\"~:a\",[1,2]]" + +;; Read data from a stream +(def in (ByteArrayInputStream. (.toByteArray out))) +(def reader (transit/reader in :json)) +(prn (transit/read reader)) ;; => "foo" +(prn (transit/read reader)) ;; => {:a [1 2]} diff --git a/test-resources/lib_tests/clj_yaml/core_test.clj b/test-resources/lib_tests/clj_yaml/core_test.clj new file mode 100644 index 00000000..0e75a4d6 --- /dev/null +++ b/test-resources/lib_tests/clj_yaml/core_test.clj @@ -0,0 +1,203 @@ +(ns clj-yaml.core-test + (:require [clojure.test :refer (deftest testing is)] + [clojure.string :as string] + [clj-yaml.core :refer [parse-string unmark generate-string]]) + (:import [java.util Date])) + +(def nested-hash-yaml + "root:\n childa: a\n childb: \n grandchild: \n greatgrandchild: bar\n") + +(def list-yaml + "--- # Favorite Movies\n- Casablanca\n- North by Northwest\n- The Man Who Wasn't There") + +(def hashes-lists-yaml " +items: + - part_no: A4786 + descrip: Water Bucket (Filled) + price: 1.47 + quantity: 4 + + - part_no: E1628 + descrip: High Heeled \"Ruby\" Slippers + price: 100.27 + quantity: 1 + owners: + - Dorthy + - Wicked Witch of the East +") + +(def inline-list-yaml +"--- # Shopping list +[milk, pumpkin pie, eggs, juice] +") + +(def inline-hash-yaml + "{name: John Smith, age: 33}") + +(def list-of-hashes-yaml " +- {name: John Smith, age: 33} +- name: Mary Smith + age: 27 +") + +(def hashes-of-lists-yaml " +men: [John Smith, Bill Jones] +women: + - Mary Smith + - Susan Williams +") + +(def typed-data-yaml " +the-bin: !!binary 0101") + +(def io-file-typed-data-yaml " +!!java.io.File") + +(def set-yaml " +--- !!set +? Mark McGwire +? Sammy Sosa +? Ken Griff") + +(deftest parse-hash + (let [parsed (parse-string "foo: bar")] + (is (= "bar" (parsed :foo))))) + +(deftest parse-hash-with-numeric-key + (let [parsed (parse-string "123: 456")] + (is (= 456 (parsed 123))))) + +(deftest parse-hash-with-complex-key + (let [parsed (parse-string "[1, 2]: 3")] + (is (= 3 (parsed [1, 2]))))) + +(deftest parse-nested-hash + (let [parsed (parse-string nested-hash-yaml)] + (is (= "a" ((parsed :root) :childa))) + (is (= "bar" ((((parsed :root) :childb) :grandchild) :greatgrandchild))))) + +(deftest parse-list + (let [parsed (parse-string list-yaml)] + (is (= "Casablanca" (first parsed))) + (is (= "North by Northwest" (nth parsed 1))) + (is (= "The Man Who Wasn't There" (nth parsed 2))))) + +(deftest parse-nested-hash-and-list + (let [parsed (parse-string hashes-lists-yaml)] + (is (= "A4786" ((first (parsed :items)) :part_no))) + (is (= "Dorthy" (first ((nth (parsed :items) 1) :owners)))))) + +(deftest parse-inline-list + (let [parsed (parse-string inline-list-yaml)] + (is (= "milk" (first parsed))) + (is (= "pumpkin pie" (nth parsed 1))) + (is (= "eggs" (nth parsed 2))) + (is (= "juice" (last parsed))))) + +(deftest parse-inline-hash + (let [parsed (parse-string inline-hash-yaml)] + (is (= "John Smith" (parsed :name))) + (is (= 33 (parsed :age))))) + +(deftest parse-list-of-hashes + (let [parsed (parse-string list-of-hashes-yaml)] + (is (= "John Smith" ((first parsed) :name))) + (is (= 33 ((first parsed) :age))) + (is (= "Mary Smith" ((nth parsed 1) :name))) + (is (= 27 ((nth parsed 1) :age))))) + +(deftest hashes-of-lists + (let [parsed (parse-string hashes-of-lists-yaml)] + (is (= "John Smith" (first (parsed :men)))) + (is (= "Bill Jones" (last (parsed :men)))) + (is (= "Mary Smith" (first (parsed :women)))) + (is (= "Susan Williams" (last (parsed :women)))))) + +(deftest h-set + (is (= #{"Mark McGwire" "Ken Griff" "Sammy Sosa"} + (parse-string set-yaml)))) + +(deftest typed-data + (let [parsed (parse-string typed-data-yaml)] + (is (= (Class/forName "[B") (type (:the-bin parsed)))))) + +(deftest disallow-arbitrary-typed-data + (is (thrown? org.yaml.snakeyaml.error.YAMLException + (parse-string io-file-typed-data-yaml)))) + +(deftest keywordized + (is (= "items" + (-> hashes-lists-yaml + (parse-string :keywords false) + ffirst)))) + +(deftest not-keywordized-in-lists + (is (every? string? + (-> "[{b: c, c: d}]" + (parse-string :keywords false) + first + keys)))) + +(deftest marking-source-position-works + (let [parsed (parse-string inline-list-yaml :mark true)] + ;; The list starts at the beginning of line 1. + (is (= 1 (-> parsed :start :line))) + (is (= 0 (-> parsed :start :column))) + ;; The first item starts at the second character of line 1. + (is (= 1 (-> parsed unmark first :start :line))) + (is (= 1 (-> parsed unmark first :start :column))) + ;; The first item ends at the fifth character of line 1. + (is (= 1 (-> parsed unmark first :end :line))) + (is (= 5 (-> parsed unmark first :end :column))))) + +(deftest text-wrapping + (let [data + {:description + "Big-picture diagram showing how our top-level systems and stakeholders interact"}] + (testing "long lines of text should not be wrapped" + ;; clj-yaml 0.5.6 used SnakeYAML 1.13 which by default did *not* split long lines. + ;; clj-yaml 0.6.0 upgraded to SnakeYAML 1.23 which by default *did* split long lines. + ;; This test ensures that generate-string uses the older behavior by default, for the sake + ;; of stability, i.e. backwards compatibility. + (is + (= "{description: Big-picture diagram showing how our top-level systems and stakeholders interact}\n" + (generate-string data)))))) + +(deftest dump-opts + (let [data [{:age 33 :name "jon"} {:age 44 :name "boo"}]] + (is (= "- age: 33\n name: jon\n- age: 44\n name: boo\n" + (generate-string data :dumper-options {:flow-style :block}))) + (is (= "[{age: 33, name: jon}, {age: 44, name: boo}]\n" + (generate-string data :dumper-options {:flow-style :flow}))))) + +;; TODO: this test is failing in GraalVM +;; Could be related to https://github.com/oracle/graal/issues/2234 +#_(deftest parse-time + (testing "clj-time parses timestamps with more than millisecond precision correctly." + (let [timestamp "2001-11-23 15:02:31.123456 -04:00" + expected 1006542151123] + (is (= (.getTime ^Date (parse-string timestamp)) expected))))) + +(deftest maps-are-ordered + (let [parsed (parse-string hashes-lists-yaml) + [first second] (:items parsed)] + (is (= (keys first) '(:part_no :descrip :price :quantity))) + (is (= (keys second)'(:part_no :descrip :price :quantity :owners))))) + + +(deftest nulls-are-fine + (testing "nil does not blow up" + (let [res (parse-string "- f:")] + (is (= [{:f nil}] res)) + (is (str res))))) + +(deftest emoji-can-be-parsed + (let [yaml "{emoji: 💣}"] + (is (= yaml (-> yaml + (generate-string) + (parse-string) + (string/trim))))) + + (testing "emoji in comments are OK too" + (let [yaml "# 💣 emoji in a comment\n42"] + (is (= 42 (parse-string yaml)))))) diff --git a/test-resources/lib_tests/clojure/data/csv_test.clj b/test-resources/lib_tests/clojure/data/csv_test.clj new file mode 100644 index 00000000..8dfa7dfb --- /dev/null +++ b/test-resources/lib_tests/clojure/data/csv_test.clj @@ -0,0 +1,79 @@ +(ns clojure.data.csv-test + (:use + [clojure.test :only (deftest is)] + [clojure.data.csv :only (read-csv write-csv)]) + (:import + [java.io Reader StringReader StringWriter EOFException])) + +(def ^{:private true} simple + "Year,Make,Model +1997,Ford,E350 +2000,Mercury,Cougar +") + +(def ^{:private true} simple-alt-sep + "Year;Make;Model +1997;Ford;E350 +2000;Mercury;Cougar +") + +(def ^{:private true} complicated + "1997,Ford,E350,\"ac, abs, moon\",3000.00 +1999,Chevy,\"Venture \"\"Extended Edition\"\"\",\"\",4900.00 +1999,Chevy,\"Venture \"\"Extended Edition, Very Large\"\"\",\"\",5000.00 +1996,Jeep,Grand Cherokee,\"MUST SELL! +air, moon roof, loaded\",4799.00") + +(deftest reading + (let [csv (read-csv simple)] + (is (= (count csv) 3)) + (is (= (count (first csv)) 3)) + (is (= (first csv) ["Year" "Make" "Model"])) + (is (= (last csv) ["2000" "Mercury" "Cougar"]))) + (let [csv (read-csv simple-alt-sep :separator \;)] + (is (= (count csv) 3)) + (is (= (count (first csv)) 3)) + (is (= (first csv) ["Year" "Make" "Model"])) + (is (= (last csv) ["2000" "Mercury" "Cougar"]))) + (let [csv (read-csv complicated)] + (is (= (count csv) 4)) + (is (= (count (first csv)) 5)) + (is (= (first csv) + ["1997" "Ford" "E350" "ac, abs, moon" "3000.00"])) + (is (= (last csv) + ["1996" "Jeep" "Grand Cherokee", "MUST SELL!\nair, moon roof, loaded" "4799.00"])))) + + +(deftest reading-and-writing + (let [string-writer (StringWriter.)] + (->> simple read-csv (write-csv string-writer)) + (is (= simple + (str string-writer))))) + +(deftest throw-if-quoted-on-eof + (let [s "ab,\"de,gh\nij,kl,mn"] + (try + (doall (read-csv s)) + (is false "No exception thrown") + (catch Exception e + (is (or (instance? java.io.EOFException e) + (and (instance? RuntimeException e) + (instance? java.io.EOFException (.getCause e))))))))) + +(deftest parse-line-endings + (let [csv (read-csv "Year,Make,Model\n1997,Ford,E350")] + (is (= 2 (count csv))) + (is (= ["Year" "Make" "Model"] (first csv))) + (is (= ["1997" "Ford" "E350"] (second csv)))) + (let [csv (read-csv "Year,Make,Model\r\n1997,Ford,E350")] + (is (= 2 (count csv))) + (is (= ["Year" "Make" "Model"] (first csv))) + (is (= ["1997" "Ford" "E350"] (second csv)))) + (let [csv (read-csv "Year,Make,Model\r1997,Ford,E350")] + (is (= 2 (count csv))) + (is (= ["Year" "Make" "Model"] (first csv))) + (is (= ["1997" "Ford" "E350"] (second csv)))) + (let [csv (read-csv "Year,Make,\"Model\"\r1997,Ford,E350")] + (is (= 2 (count csv))) + (is (= ["Year" "Make" "Model"] (first csv))) + (is (= ["1997" "Ford" "E350"] (second csv))))) diff --git a/test/babashka/async_test.clj b/test/babashka/async_test.clj new file mode 100644 index 00000000..783e7788 --- /dev/null +++ b/test/babashka/async_test.clj @@ -0,0 +1,35 @@ +(ns babashka.async-test + (:require + [babashka.test-utils :as test-utils] + [clojure.edn :as edn] + [clojure.test :as t :refer [deftest is]])) + +(deftest alts!!-test + (is (= "process 2\n" (test-utils/bb nil " + (defn async-command [& args] + (async/thread (apply shell/sh \"bash\" \"-c\" args))) + + (-> (async/alts!! [(async-command \"sleep 2 && echo process 1\") + (async-command \"sleep 1 && echo process 2\")]) + first :out str/trim println)")))) + +(deftest go-test + (is (number? (edn/read-string (test-utils/bb nil " +(defn calculation-go [] + (async/go + ;; wait for some stuff + (rand-int 1000))) + +(defn get-result-go [] + (async/go + (->> + (repeatedly 10 calculation-go) + (map async/ response :args :foo))) -"))))) +"))) + (catch Exception e + (str/includes? (str e) "timed out"))))) diff --git a/test/babashka/impl/nrepl_server_test.clj b/test/babashka/impl/nrepl_server_test.clj new file mode 100644 index 00000000..52613754 --- /dev/null +++ b/test/babashka/impl/nrepl_server_test.clj @@ -0,0 +1,203 @@ +(ns babashka.impl.nrepl-server-test + (:require + [babashka.impl.bencode.core :as bencode] + [babashka.impl.nrepl-server :refer [start-server! stop-server!]] + [babashka.main :as main] + [babashka.test-utils :as tu] + [babashka.wait :as wait] + [clojure.test :as t :refer [deftest is testing]] + [sci.impl.opts :refer [init]]) + (:import [java.lang ProcessBuilder$Redirect])) + +(def debug? false) + +(set! *warn-on-reflection* true) + +(defn bytes->str [x] + (if (bytes? x) (String. (bytes x)) + (str x))) + +(defn read-msg [msg] + (let [res (zipmap (map keyword (keys msg)) + (map #(if (bytes? %) + (String. (bytes %)) + %) + (vals msg))) + res (if-let [status (:status res)] + (assoc res :status (mapv bytes->str status)) + res) + res (if-let [status (:sessions res)] + (assoc res :sessions (mapv bytes->str status)) + res)] + res)) + +(defn read-reply [in session id] + (loop [] + (let [msg (read-msg (bencode/read-bencode in))] + (if (and (= (:session msg) session) + (= (:id msg) id)) + (do + (when debug? (prn "received" msg)) + msg) + (do + (when debug? (prn "skipping over msg" msg)) + (recur)))))) + +(defn nrepl-test [] + (with-open [socket (java.net.Socket. "127.0.0.1" 1667) + in (.getInputStream socket) + in (java.io.PushbackInputStream. in) + os (.getOutputStream socket)] + (bencode/write-bencode os {"op" "clone"}) + (let [session (:new-session (read-msg (bencode/read-bencode in))) + id (atom 0) + new-id! #(swap! id inc)] + (testing "session" + (is session)) + (testing "eval" + (bencode/write-bencode os {"op" "eval" "code" "(+ 1 2 3)" "session" session "id" (new-id!)}) + (let [msg (read-reply in session @id) + id (:id msg) + value (:value msg)] + (is (= 1 id)) + (is (= value "6"))) + (testing "creating a namespace and evaluating something in it" + (bencode/write-bencode os {"op" "eval" + "code" "(ns ns0) (defn foo [] :foo0) (ns ns1) (defn foo [] :foo1)" + "session" session + "id" (new-id!)}) + (read-reply in session @id) + (testing "not providing the ns key evaluates in the last defined namespace" + (bencode/write-bencode os {"op" "eval" "code" "(foo)" "session" session "id" (new-id!)}) + (is (= ":foo1" (:value (read-reply in session @id))))) + (testing "explicitly providing the ns key evaluates in that namespace" + (bencode/write-bencode os {"op" "eval" + "code" "(foo)" + "session" session + "id" (new-id!) + "ns" "ns0"}) + (is (= ":foo0" (:value (read-reply in session @id))))) + (testing "providing an ns value of a non-existing namespace creates the namespace" + (bencode/write-bencode os {"op" "eval" + "code" "(ns-name *ns*)" + "session" session + "id" (new-id!) + "ns" "unicorn"}) + (let [reply (read-reply in session @id)] + (is (= "unicorn" (:value reply)))))) + (testing "multiple top level expressions results in two value replies" + (bencode/write-bencode os {"op" "eval" + "code" "(+ 1 2 3) (+ 1 2 3)" + "session" session + "id" (new-id!)}) + (let [reply-1 (read-reply in session @id) + reply-2 (read-reply in session @id)] + (is (= "6" (:value reply-1) (:value reply-2)))))) + (testing "load-file" + (bencode/write-bencode os {"op" "load-file" "file" "(ns foo) (defn foo [] :foo)" "session" session "id" (new-id!)}) + (read-reply in session @id) + (bencode/write-bencode os {"op" "eval" "code" "(foo)" "ns" "foo" "session" session "id" (new-id!)}) + (is (= ":foo" (:value (read-reply in session @id))))) + (testing "complete" + (testing "completions for fo" + (bencode/write-bencode os {"op" "complete" + "symbol" "fo" + "session" session + "id" (new-id!) + "ns" "foo"}) + (let [reply (read-reply in session @id) + completions (:completions reply) + completions (mapv read-msg completions) + completions (into #{} (map (juxt :ns :candidate)) completions)] + (is (contains? completions ["foo" "foo"])) + (is (contains? completions ["clojure.core" "format"])))) + (testing "completions for quux should be empty" + (bencode/write-bencode os {"op" "complete" + "symbol" "quux" + "session" session "id" (new-id!) + "ns" "foo"}) + (let [reply (read-reply in session @id) + completions (:completions reply)] + (is (empty? completions))) + (testing "unless quux is an alias" + (bencode/write-bencode os {"op" "eval" "code" "(require '[cheshire.core :as quux])" "session" session "id" (new-id!)}) + (read-reply in session @id) + (bencode/write-bencode os {"op" "complete" "symbol" "quux" "session" session "id" (new-id!)}) + (let [reply (read-reply in session @id) + completions (:completions reply) + completions (mapv read-msg completions) + completions (into #{} (map (juxt :ns :candidate)) completions)] + (is (contains? completions ["cheshire.core" "quux/generate-string"]))))) + (testing "completions for clojure.test" + (bencode/write-bencode os {"op" "eval" "code" "(require '[clojure.test :as test])" "session" session "id" (new-id!)}) + (read-reply in session @id) + (bencode/write-bencode os {"op" "complete" "symbol" "test" "session" session "id" (new-id!)}) + (let [reply (read-reply in session @id) + completions (:completions reply) + completions (mapv read-msg completions) + completions (into #{} (map (juxt :ns :candidate)) completions)] + (is (contains? completions ["clojure.test" "test/deftest"]))))) + (testing "close + ls-sessions" + (bencode/write-bencode os {"op" "ls-sessions" "session" session "id" (new-id!)}) + (let [reply (read-reply in session @id) + sessions (set (:sessions reply))] + (is (contains? sessions session)) + (let [new-sessions (loop [i 0 + sessions #{}] + (bencode/write-bencode os {"op" "clone" "session" session "id" (new-id!)}) + (let [new-session (:new-session (read-reply in session @id)) + sessions (conj sessions new-session)] + (if (= i 4) + sessions + (recur (inc i) sessions))))] + (bencode/write-bencode os {"op" "ls-sessions" "session" session "id" (new-id!)}) + (let [reply (read-reply in session @id) + sessions (set (:sessions reply))] + (is (= 6 (count sessions))) + (is (contains? sessions session)) + (is (= new-sessions (disj sessions session))) + (testing "close" + (doseq [close-session (disj sessions session)] + (bencode/write-bencode os {"op" "close" "session" close-session "id" (new-id!)}) + (let [reply (read-reply in close-session @id)] + (is (contains? (set (:status reply)) "session-closed"))))) + (testing "session not listen in ls-sessions after close" + (bencode/write-bencode os {"op" "ls-sessions" "session" session "id" (new-id!)}) + (let [reply (read-reply in session @id) + sessions (set (:sessions reply))] + (is (contains? sessions session)) + (is (not (some #(contains? sessions %) new-sessions))))))))) + (testing "output" + (bencode/write-bencode os {"op" "eval" "code" "(dotimes [i 3] (println \"Hello\"))" + "session" session "id" (new-id!)}) + (dotimes [_ 3] + (let [reply (read-reply in session @id)] + (is (= "Hello\n" (:out reply))))))))) + +(deftest nrepl-server-test + (let [proc-state (atom nil)] + (try + (if tu/jvm? + (future + (start-server! + (init {:namespaces main/namespaces + :features #{:bb}}) "0.0.0.0:1667")) + (let [pb (ProcessBuilder. ["./bb" "--nrepl-server" "0.0.0.0:1667"]) + _ (.redirectError pb ProcessBuilder$Redirect/INHERIT) + ;; _ (.redirectOutput pb ProcessBuilder$Redirect/INHERIT) + ;; env (.environment pb) + ;; _ (.put env "BABASHKA_DEV" "true") + proc (.start pb)] + (reset! proc-state proc))) + (babashka.wait/wait-for-port "localhost" 1667) + (nrepl-test) + (finally + (if tu/jvm? + (stop-server!) + (when-let [proc @proc-state] + (.destroy ^Process proc))))))) + +;;;; Scratch + +(comment + ) diff --git a/test/babashka/impl/repl_test.clj b/test/babashka/impl/repl_test.clj index 29a932c8..b914207b 100644 --- a/test/babashka/impl/repl_test.clj +++ b/test/babashka/impl/repl_test.clj @@ -3,8 +3,8 @@ [babashka.impl.repl :refer [start-repl!]] [clojure.string :as str] [clojure.test :as t :refer [deftest is]] - [sci.impl.opts :refer [init]] [sci.core :as sci] + [sci.impl.opts :refer [init]] [sci.impl.vars :as vars])) (set! *warn-on-reflection* true) @@ -24,6 +24,15 @@ (sci/with-in-str (str expr "\n:repl/quit") (repl!))) expected))) +(defn assert-repl-error [expr expected] + (is (str/includes? + (let [sw (java.io.StringWriter.)] + (sci/binding [sci/out (java.io.StringWriter.) + sci/err sw] + (sci/with-in-str (str expr "\n:repl/quit") + (repl!))) + (str sw)) expected))) + (deftest repl-test (assert-repl "1" "1") (assert-repl "[1 2 3]" "[1 2 3]") @@ -34,7 +43,8 @@ (assert-repl "1\n(inc *1)" "2") (assert-repl "1\n(dec *1)(+ *2 *2)" "2") (assert-repl "1\n(dec *1)(+ *2 *2)" "2") - (assert-repl "*command-line-args*" "[\"a\" \"b\" \"c\"]")) + (assert-repl "*command-line-args*" "[\"a\" \"b\" \"c\"]") + (assert-repl-error "(+ 1 nil)" "NullPointerException")) ;;;; Scratch diff --git a/test/babashka/impl/socket_repl_test.clj b/test/babashka/impl/socket_repl_test.clj index 8bad7823..18abb4e4 100644 --- a/test/babashka/impl/socket_repl_test.clj +++ b/test/babashka/impl/socket_repl_test.clj @@ -2,10 +2,10 @@ (:require [babashka.impl.socket-repl :refer [start-repl! stop-repl!]] [babashka.test-utils :as tu] + [clojure.java.io :as io] [clojure.java.shell :refer [sh]] [clojure.string :as str] [clojure.test :as t :refer [deftest is testing]] - [clojure.java.io :as io] [sci.impl.opts :refer [init]])) (set! *warn-on-reflection* true) diff --git a/test/babashka/java_time_test.clj b/test/babashka/java_time_test.clj index 65b22709..c66fb8e4 100644 --- a/test/babashka/java_time_test.clj +++ b/test/babashka/java_time_test.clj @@ -16,4 +16,8 @@ (is (= "18-12-2019 16:01:41" (bb '(.format (java.time.LocalDateTime/parse "2019-12-18T16:01:41.485") - (java.time.format.DateTimeFormatter/ofPattern "dd-MM-yyyy HH:mm:ss")))))) + (java.time.format.DateTimeFormatter/ofPattern "dd-MM-yyyy HH:mm:ss"))))) + (is (number? (bb " +(let [x (java.time.LocalDateTime/parse \"2019-12-18T16:01:41.485\") + y (java.time.LocalDateTime/now)] + (.between java.time.temporal.ChronoUnit/MINUTES x y))")))) diff --git a/test/babashka/main_test.clj b/test/babashka/main_test.clj index 6d5c3587..e9b43336 100644 --- a/test/babashka/main_test.clj +++ b/test/babashka/main_test.clj @@ -9,6 +9,10 @@ [clojure.test :as test :refer [deftest is testing]] [sci.core :as sci])) +(defmethod clojure.test/report :begin-test-var [m] + (println "===" (-> m :var meta :name)) + (println)) + (defn bb [input & args] (edn/read-string (apply test-utils/bb (when (some? input) (str input)) (map str args)))) @@ -29,7 +33,7 @@ (deftest main-test (testing "-io behaves as identity" - (= "foo\nbar\n" (test-utils/bb "foo\nbar\n" "-io" "*input*"))) + (is (= "foo\nbar\n" (test-utils/bb "foo\nbar\n" "-io" "*input*")))) (testing "if and when" (is (= 1 (bb 0 '(if (zero? *input*) 1 2)))) (is (= 2 (bb 1 '(if (zero? *input*) 1 2)))) @@ -118,9 +122,39 @@ (deftest load-file-test (let [tmp (java.io.File/createTempFile "script" ".clj")] - (spit tmp "(defn foo [x y] (+ x y)) (defn bar [x y] (* x y))") - (is (= "120\n" (test-utils/bb nil (format "(load-file \"%s\") (bar (foo 10 30) 3)" - (.getPath tmp))))))) + (.deleteOnExit tmp) + (spit tmp "(ns foo) (defn foo [x y] (+ x y)) (defn bar [x y] (* x y))") + (is (= "120\n" (test-utils/bb nil (format "(load-file \"%s\") (foo/bar (foo/foo 10 30) 3)" + (.getPath tmp))))) + (testing "namespace is restored after load file" + (is (= 'start-ns + (bb nil (format "(ns start-ns) (load-file \"%s\") (ns-name *ns*)" + (.getPath tmp)))))))) + +(deftest repl-source-test + (let [tmp (java.io.File/createTempFile "lib" ".clj") + name (str/replace (.getName tmp) ".clj" "") + dir (.getParent tmp)] + (.deleteOnExit tmp) + (testing "print source from loaded file" + (spit tmp (format " +(ns %s) + +(defn foo [x y] + (+ x y))" name)) + (is (= "(defn foo [x y]\n (+ x y))\n" + (bb nil (format " +(load-file \"%s\") +(require '[clojure.repl :refer [source]]) +(with-out-str (source %s/foo))" + (.getPath tmp) + name))))) + (testing "print source from file on classpath" + (is (= "(defn foo [x y]\n (+ x y))\n" + (bb nil + "-cp" dir + "-e" (format "(require '[clojure.repl :refer [source]] '[%s])" name) + "-e" (format "(with-out-str (source %s/foo))" name))))))) (deftest eval-test (is (= "120\n" (test-utils/bb nil "(eval '(do (defn foo [x y] (+ x y)) @@ -160,7 +194,7 @@ (deftest future-test (is (= 6 (bb nil "@(future (+ 1 2 3))")))) - + (deftest promise-test (is (= :timeout (bb nil "(deref (promise) 1 :timeout)"))) (is (= :ok (bb nil "(let [x (promise)] @@ -220,23 +254,26 @@ {:default :timed-out :timeout 100}))" temp-dir-path)))))) -(deftest async-test - (is (= "process 2\n" (test-utils/bb nil " - (defn async-command [& args] - (async/thread (apply shell/sh \"bash\" \"-c\" args))) - - (-> (async/alts!! [(async-command \"sleep 2 && echo process 1\") - (async-command \"sleep 1 && echo process 2\")]) - first :out str/trim println)")))) - (deftest tools-cli-test (is (= {:result 8080} (bb nil "test/babashka/scripts/tools.cli.bb")))) (deftest try-catch-test - (is (zero? (bb nil "(try (/ 1 0) (catch ArithmeticException _ 0))")))) + (is (zero? (bb nil "(try (/ 1 0) (catch ArithmeticException _ 0))"))) + (is (= :got-it (bb nil " +(defn foo [] + (throw (java.util.MissingResourceException. \"o noe!\" \"\" \"\"))) + +(defn bar + [] + (try (foo) + (catch java.util.MissingResourceException _ + :got-it))) +(bar) +")))) (deftest reader-conditionals-test (is (= :hello (bb nil "#?(:bb :hello :default :bye)"))) + (is (= :hello (bb nil "#? (:bb :hello :default :bye)"))) (is (= :hello (bb nil "#?(:clj :hello :bb :bye)"))) (is (= [1 2] (bb nil "[1 2 #?@(:bb [] :clj [1])]")))) @@ -355,8 +392,52 @@ (alter-var-root #'clojure.core/inc (constantly inc2)) res)"))))) +(deftest pprint-test + (testing "writer" + (is (string? (bb nil "(let [sw (java.io.StringWriter.)] (clojure.pprint/pprint (range 10) sw) (str sw))"))))) + +(deftest read-string-test + (testing "namespaced keyword via alias" + (is (= :clojure.string/foo + (bb nil "(ns foo (:require [clojure.string :as str])) (read-string \"::str/foo\")"))))) + +(deftest available-stream-test + (is (= 0 (bb nil "(.available System/in)")))) + +(deftest file-reader-test + (when (str/includes? (str/lower-case (System/getProperty "os.name")) "linux") + (let [v (bb nil "(slurp (io/reader (java.io.FileReader. \"/proc/loadavg\")))")] + (prn "output:" v) + (is v)))) + +(deftest download-and-extract-test + (is (try (= 6 (bb nil (io/file "test" "babashka" "scripts" "download_and_extract_zip.bb"))) + (catch Exception e + (is (str/includes? (str e) "timed out")))))) + +(deftest get-message-on-exception-info-test + (is "foo" (bb nil "(try (throw (ex-info \"foo\" {})) (catch Exception e (.getMessage e)))"))) + +(deftest pushback-reader-test + (is (= "foo" (bb nil " +(require '[clojure.java.io :as io]) +(let [pb (java.io.PushbackInputStream. (java.io.ByteArrayInputStream. (.getBytes \"foo\")))] + (.unread pb (.read pb)) + (slurp pb))")))) + +(deftest delete-on-exit-test + (when test-utils/native? + (let [f (java.io.File/createTempFile "foo" "bar") + p (.getPath f)] + (bb nil (format "(.deleteOnExit (io/file \"%s\"))" p)) + (is (false? (.exists f)))))) + +(deftest yaml-test + (is (str/starts-with? + (bb nil "(yaml/generate-string [{:name \"John Smith\", :age 33} {:name \"Mary Smith\", :age 27}])") + "-"))) + ;;;; Scratch (comment - (dotimes [_ 10] (wait-for-port-test)) - ) + (dotimes [_ 10] (wait-for-port-test))) diff --git a/test/babashka/scripts/download_and_extract_zip.bb b/test/babashka/scripts/download_and_extract_zip.bb new file mode 100644 index 00000000..befe9258 --- /dev/null +++ b/test/babashka/scripts/download_and_extract_zip.bb @@ -0,0 +1,30 @@ +(require '[clojure.java.io :as io] '[clojure.java.shell :refer [sh]] '[clojure.string :as str]) +(import '[java.net URL HttpURLConnection]) + +(set! *warn-on-reflection* true) + +(let [os-name (System/getProperty "os.name") + os-name (str/lower-case os-name) + os (cond (str/includes? os-name "linux") "linux" + (str/includes? os-name "mac") "macos" + (str/includes? os-name "win") "windows") + tmp-dir (System/getProperty "java.io.tmpdir") + zip-file (io/file tmp-dir "bb-0.0.78.zip") + source (URL. (format "https://github.com/borkdude/babashka/releases/download/v0.0.78/babashka-0.0.78-%s-amd64.zip" os)) + conn ^HttpURLConnection (.openConnection ^URL source) + _ (.setConnectTimeout conn 2000) + _ (.setReadTimeout conn 2000)] + (.connect conn) + (with-open [is (.getInputStream conn)] + (io/copy is zip-file)) + (let [bb-file (io/file tmp-dir "bb-extracted") + fs (java.nio.file.FileSystems/newFileSystem (.toPath zip-file) nil) + to-extract (.getPath fs "bb" (into-array String []))] + (java.nio.file.Files/copy to-extract (.toPath bb-file) + ^"[Ljava.nio.file.CopyOption;" + (into-array java.nio.file.CopyOption [])) + (.setExecutable bb-file true) + (let [out (:out (sh (.getPath bb-file) "(+ 1 2 3)"))] + (.delete bb-file) + (.delete zip-file) + (println out)))) diff --git a/test/babashka/scripts/interrupt_handler.bb b/test/babashka/scripts/interrupt_handler.bb new file mode 100644 index 00000000..02291139 --- /dev/null +++ b/test/babashka/scripts/interrupt_handler.bb @@ -0,0 +1,6 @@ +(require '[babashka.signal :as signal]) + +(signal/add-interrupt-handler! :quit (fn [k] (println "bye1" k))) +(signal/add-interrupt-handler! :quit2 (fn [k] (println "bye2" k))) + +(System/exit 0) diff --git a/test/babashka/shutdown_hook_test.clj b/test/babashka/shutdown_hook_test.clj new file mode 100644 index 00000000..71995c97 --- /dev/null +++ b/test/babashka/shutdown_hook_test.clj @@ -0,0 +1,24 @@ +(ns babashka.shutdown-hook-test + {:no-doc true} + (:import [java.nio.charset Charset]) + (:require [babashka.test-utils :as tu] + [clojure.java.io :as io] + [clojure.test :refer [deftest is]])) + +(defn- stream-to-string + ([in] (stream-to-string in (.name (Charset/defaultCharset)))) + ([in enc] + (with-open [bout (java.io.StringWriter.)] + (io/copy in bout :encoding enc) + (.toString bout)))) + +(deftest shutdown-hook-test + (let [script "(-> (Runtime/getRuntime) (.addShutdownHook (Thread. #(println \"bye\"))))" + pb (ProcessBuilder. (if tu/jvm? + ["lein" "bb" "-e" script] + ["./bb" "-e" script])) + process (.start pb) + output (.getInputStream process)] + (when-let [s (not-empty (stream-to-string (.getErrorStream process)))] + (prn "ERROR:" s)) + (is (= "bye\n" (stream-to-string output))))) diff --git a/test/babashka/transit_test.clj b/test/babashka/transit_test.clj new file mode 100644 index 00000000..86523771 --- /dev/null +++ b/test/babashka/transit_test.clj @@ -0,0 +1,13 @@ +(ns babashka.transit-test + (:require + [babashka.test-utils :as test-utils] + [clojure.java.io :as io] + [clojure.test :as t :refer [deftest is]])) + +(defn bb [& args] + (apply test-utils/bb nil (map str args))) + +(deftest transit-test + (is (= "\"foo\"\n{:a [1 2]}\n" + (bb (format "(load-file \"%s\")" + (.getPath (io/file "test-resources" "babashka" "transit.clj"))))))) diff --git a/test/babashka/udp_test.clj b/test/babashka/udp_test.clj new file mode 100644 index 00000000..5c0f3730 --- /dev/null +++ b/test/babashka/udp_test.clj @@ -0,0 +1,25 @@ +(ns babashka.udp-test + (:require [babashka.test-utils :as tu] + [clojure.test :refer [deftest is]]) + (:import [java.io StringWriter] + [java.net DatagramPacket DatagramSocket])) + +(set! *warn-on-reflection* true) + +(deftest udp-test + (let [server (DatagramSocket. 8125) + sw (StringWriter.) + fut (future + (let [buf (byte-array 1024) + packet (DatagramPacket. buf 1024) + _ (.receive server packet) + non-zero-bytes (filter #(not (zero? %)) (.getData packet)) + non-zero-bytes (byte-array non-zero-bytes)] + (binding [*out* sw] + (println (String. non-zero-bytes "UTF-8")))))] + (while (not (realized? fut)) + (tu/bb nil + "-e" "(load-file (io/file \"test-resources\" \"babashka\" \"statsd.clj\"))" + "-e" "(require '[statsd-client :as c])" + "-e" "(c/increment :foo)")) + (is (= ":foo:1|c\n" (str sw)))))